diff options
author | Doug Bunting <6431421+dougbu@users.noreply.github.com> | 2019-10-02 20:25:59 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-02 20:25:59 +0300 |
commit | a3ac1ce895cd20953e461e80a9712f0e645c68b8 (patch) | |
tree | e1fbd904fd5769ce3539eba8b750797e2f3d07f3 /src | |
parent | 43456141e8f5e4ccfc64e89bef49d59cb94e768f (diff) | |
parent | cd2983bf414a3432a96daafc50dad162432e3131 (diff) |
Merge branch 'master' into merge/release/3.1-to-master
Diffstat (limited to 'src')
325 files changed, 3251 insertions, 1198 deletions
diff --git a/src/Components/Analyzers/test/ComponentInternalUsageDiagnoticsAnalyzerTest.cs b/src/Components/Analyzers/test/ComponentInternalUsageDiagnosticsAnalyzerTest.cs index 92e2252304..e478f1ef45 100644 --- a/src/Components/Analyzers/test/ComponentInternalUsageDiagnoticsAnalyzerTest.cs +++ b/src/Components/Analyzers/test/ComponentInternalUsageDiagnosticsAnalyzerTest.cs @@ -8,9 +8,9 @@ using Xunit; namespace Microsoft.AspNetCore.Components.Analyzers { - public class ComponentInternalUsageDiagnoticsAnalyzerTest : AnalyzerTestBase + public class ComponentInternalUsageDiagnosticsAnalyzerTest : AnalyzerTestBase { - public ComponentInternalUsageDiagnoticsAnalyzerTest() + public ComponentInternalUsageDiagnosticsAnalyzerTest() { Analyzer = new ComponentInternalUsageDiagnosticAnalyzer(); Runner = new ComponentAnalyzerDiagnosticAnalyzerRunner(Analyzer); diff --git a/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs index 415030a011..453cd69d74 100644 --- a/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs +++ b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components.RenderTree; -namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnoticsAnalyzerTest +namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnosticsAnalyzerTest { class UsesRenderTreeFrameAsParameter { diff --git a/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs index bdd40c2df1..daf857f996 100644 --- a/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs +++ b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs @@ -1,7 +1,7 @@ using System; using Microsoft.AspNetCore.Components.RenderTree; -namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnoticsAnalyzerTest +namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnosticsAnalyzerTest { class UsesRenderTreeFrameTypeAsLocal { diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs index d08162a590..5182a1660d 100644 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs +++ b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting public static void Run(this IWebAssemblyHost host) { // Behave like async void, because we don't yet support async-main properly on WebAssembly. - // However, don't actualy make this method async, because we rely on startup being synchronous + // However, don't actually make this method async, because we rely on startup being synchronous // for things like attaching navigation event handlers. host.StartAsync().ContinueWith(task => { diff --git a/src/Components/Blazor/Blazor/src/Services/WebAssemblyConsoleLogger.cs b/src/Components/Blazor/Blazor/src/Services/WebAssemblyConsoleLogger.cs index c86c1cf30b..1769dbd915 100644 --- a/src/Components/Blazor/Blazor/src/Services/WebAssemblyConsoleLogger.cs +++ b/src/Components/Blazor/Blazor/src/Services/WebAssemblyConsoleLogger.cs @@ -20,6 +20,11 @@ namespace Microsoft.AspNetCore.Blazor.Services public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { + if (!IsEnabled(logLevel)) + { + return; + } + var formattedMessage = formatter(state, exception); Console.WriteLine($"[{logLevel}] {formattedMessage}"); } diff --git a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets index 96a844817e..42ef903f15 100644 --- a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets +++ b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets @@ -436,9 +436,15 @@ <!-- Clear the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker --> <Delete Files="$(BlazorIntermediateLinkerOutputPath)*.dll" /> + + + <PropertyGroup> + <_MonoLinkerDotNetPath>$(DOTNET_HOST_PATH)</_MonoLinkerDotNetPath> + <_MonoLinkerDotNetPath Condition="'$(_MonoLinkerDotNetPath)' == ''">dotnet</_MonoLinkerDotNetPath> + </PropertyGroup> <!-- Run the linker and put the results in /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker --> - <Exec Command="dotnet "$(MonoLinkerPath)" $(_BlazorLinkerAdditionalOptions) @(_BlazorFolderLookupPaths, ' ') -o "$(BlazorIntermediateLinkerOutputPath)" @(_BlazorAssemblyDescriptorFiles, ' ') @(_BlazorAssembliesToLink, ' ')" /> + <Exec Command="$(_MonoLinkerDotNetPath) "$(MonoLinkerPath)" $(_BlazorLinkerAdditionalOptions) @(_BlazorFolderLookupPaths, ' ') -o "$(BlazorIntermediateLinkerOutputPath)" @(_BlazorAssemblyDescriptorFiles, ' ') @(_BlazorAssembliesToLink, ' ')" /> <!-- Collect the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker/ --> <ItemGroup> diff --git a/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs b/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs index d15cf4f584..9e924cb7b1 100644 --- a/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs +++ b/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs @@ -96,7 +96,7 @@ namespace Test } [Fact] - public void Render_ChildComponent_TriesToSetNonParamter() + public void Render_ChildComponent_TriesToSetNonParameter() { // Arrange AdditionalSyntaxTrees.Add(Parse(@" diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs index 348b69c691..e6df317bd9 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs @@ -100,7 +100,7 @@ namespace WsProxy { break; } case "Debugger.paused": { - //TODO figure out how to stich out more frames and, in particular what happens when real wasm is on the stack + //TODO figure out how to stitch out more frames and, in particular what happens when real wasm is on the stack var top_func = args? ["callFrames"]? [0]? ["functionName"]?.Value<string> (); if (top_func == "mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_bp") { await OnBreakPointHit (args, token); @@ -419,7 +419,7 @@ namespace WsProxy { var res = await SendCommand("Runtime.evaluate", o, token); - //if we fail we just buble that to the IDE (and let it panic over it) + //if we fail we just bubble that to the IDE (and let it panic over it) if (res.IsErr) { SendResponse(msg_id, res, token); @@ -475,7 +475,7 @@ namespace WsProxy { var res = await SendCommand ("Runtime.evaluate", o, token); - //if we fail we just buble that to the IDE (and let it panic over it) + //if we fail we just bubble that to the IDE (and let it panic over it) if (res.IsErr) { SendResponse (msg_id, res, token); return; @@ -594,7 +594,7 @@ namespace WsProxy { var res = await EnableBreakPoint (bp, token); var ret_code = res.Value? ["result"]? ["value"]?.Value<int> (); - //if we fail we just buble that to the IDE (and let it panic over it) + //if we fail we just bubble that to the IDE (and let it panic over it) if (!ret_code.HasValue) { //FIXME figure out how to inform the IDE of that. Info ($"FAILED TO ENABLE BP {bp.LocalId}"); @@ -668,7 +668,7 @@ namespace WsProxy { var res = await EnableBreakPoint (bp, token); var ret_code = res.Value? ["result"]? ["value"]?.Value<int> (); - //if we fail we just buble that to the IDE (and let it panic over it) + //if we fail we just bubble that to the IDE (and let it panic over it) if (!ret_code.HasValue) { SendResponse (msg_id, res, token); return; diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/.template.config.src/template.json b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/.template.config.src/template.json index 33e094e356..119303324e 100644 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/.template.config.src/template.json +++ b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/.template.config.src/template.json @@ -83,12 +83,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "HostIdentifier": { "type": "bind", diff --git a/src/Components/Components/src/BindConverter.cs b/src/Components/Components/src/BindConverter.cs index f77689bde2..30ded5fe73 100644 --- a/src/Components/Components/src/BindConverter.cs +++ b/src/Components/Components/src/BindConverter.cs @@ -430,7 +430,7 @@ namespace Microsoft.AspNetCore.Components private static string FormatEnumValueCore<T>(T value, CultureInfo culture) where T : struct, Enum { - return value.ToString(); // The overload that acccepts a culture is [Obsolete] + return value.ToString(); // The overload that accepts a culture is [Obsolete] } private static string FormatNullableEnumValueCore<T>(T? value, CultureInfo culture) where T : struct, Enum @@ -440,7 +440,7 @@ namespace Microsoft.AspNetCore.Components return null; } - return value.Value.ToString(); // The overload that acccepts a culture is [Obsolete] + return value.Value.ToString(); // The overload that accepts a culture is [Obsolete] } /// <summary> @@ -1166,99 +1166,99 @@ namespace Microsoft.AspNetCore.Components public static BindFormatter<T> Get<T>() { - if (!_cache.TryGetValue(typeof(T), out var formattter)) + if (!_cache.TryGetValue(typeof(T), out var formatter)) { // We need to replicate all of the primitive cases that we handle here so that they will behave the same way. // The result will be cached. if (typeof(T) == typeof(string)) { - formattter = (BindFormatter<string>)FormatStringValueCore; + formatter = (BindFormatter<string>)FormatStringValueCore; } else if (typeof(T) == typeof(bool)) { - formattter = (BindFormatter<bool>)FormatBoolValueCore; + formatter = (BindFormatter<bool>)FormatBoolValueCore; } else if (typeof(T) == typeof(bool?)) { - formattter = (BindFormatter<bool?>)FormatNullableBoolValueCore; + formatter = (BindFormatter<bool?>)FormatNullableBoolValueCore; } else if (typeof(T) == typeof(int)) { - formattter = (BindFormatter<int>)FormatIntValueCore; + formatter = (BindFormatter<int>)FormatIntValueCore; } else if (typeof(T) == typeof(int?)) { - formattter = (BindFormatter<int?>)FormatNullableIntValueCore; + formatter = (BindFormatter<int?>)FormatNullableIntValueCore; } else if (typeof(T) == typeof(long)) { - formattter = (BindFormatter<long>)FormatLongValueCore; + formatter = (BindFormatter<long>)FormatLongValueCore; } else if (typeof(T) == typeof(long?)) { - formattter = (BindFormatter<long?>)FormatNullableLongValueCore; + formatter = (BindFormatter<long?>)FormatNullableLongValueCore; } else if (typeof(T) == typeof(float)) { - formattter = (BindFormatter<float>)FormatFloatValueCore; + formatter = (BindFormatter<float>)FormatFloatValueCore; } else if (typeof(T) == typeof(float?)) { - formattter = (BindFormatter<float?>)FormatNullableFloatValueCore; + formatter = (BindFormatter<float?>)FormatNullableFloatValueCore; } else if (typeof(T) == typeof(double)) { - formattter = (BindFormatter<double>)FormatDoubleValueCore; + formatter = (BindFormatter<double>)FormatDoubleValueCore; } else if (typeof(T) == typeof(double?)) { - formattter = (BindFormatter<double?>)FormatNullableDoubleValueCore; + formatter = (BindFormatter<double?>)FormatNullableDoubleValueCore; } else if (typeof(T) == typeof(decimal)) { - formattter = (BindFormatter<decimal>)FormatDecimalValueCore; + formatter = (BindFormatter<decimal>)FormatDecimalValueCore; } else if (typeof(T) == typeof(decimal?)) { - formattter = (BindFormatter<decimal?>)FormatNullableDecimalValueCore; + formatter = (BindFormatter<decimal?>)FormatNullableDecimalValueCore; } else if (typeof(T) == typeof(DateTime)) { - formattter = (BindFormatter<DateTime>)FormatDateTimeValueCore; + formatter = (BindFormatter<DateTime>)FormatDateTimeValueCore; } else if (typeof(T) == typeof(DateTime?)) { - formattter = (BindFormatter<DateTime?>)FormatNullableDateTimeValueCore; + formatter = (BindFormatter<DateTime?>)FormatNullableDateTimeValueCore; } else if (typeof(T) == typeof(DateTimeOffset)) { - formattter = (BindFormatter<DateTimeOffset>)FormatDateTimeOffsetValueCore; + formatter = (BindFormatter<DateTimeOffset>)FormatDateTimeOffsetValueCore; } else if (typeof(T) == typeof(DateTimeOffset?)) { - formattter = (BindFormatter<DateTimeOffset?>)FormatNullableDateTimeOffsetValueCore; + formatter = (BindFormatter<DateTimeOffset?>)FormatNullableDateTimeOffsetValueCore; } else if (typeof(T).IsEnum) { // We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse. var method = _formatEnumValue ??= typeof(BindConverter).GetMethod(nameof(FormatEnumValueCore), BindingFlags.NonPublic | BindingFlags.Static); - formattter = method.MakeGenericMethod(typeof(T)).CreateDelegate(typeof(BindFormatter<T>), target: null); + formatter = method.MakeGenericMethod(typeof(T)).CreateDelegate(typeof(BindFormatter<T>), target: null); } else if (Nullable.GetUnderlyingType(typeof(T)) is Type innerType && innerType.IsEnum) { // We have to deal invoke this dynamically to work around the type constraint on Enum.TryParse. var method = _formatNullableEnumValue ??= typeof(BindConverter).GetMethod(nameof(FormatNullableEnumValueCore), BindingFlags.NonPublic | BindingFlags.Static); - formattter = method.MakeGenericMethod(innerType).CreateDelegate(typeof(BindFormatter<T>), target: null); + formatter = method.MakeGenericMethod(innerType).CreateDelegate(typeof(BindFormatter<T>), target: null); } else { - formattter = MakeTypeConverterFormatter<T>(); + formatter = MakeTypeConverterFormatter<T>(); } - _cache.TryAdd(typeof(T), formattter); + _cache.TryAdd(typeof(T), formatter); } - return (BindFormatter<T>)formattter; + return (BindFormatter<T>)formatter; } private static BindFormatter<T> MakeTypeConverterFormatter<T>() diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs index db03b3d416..605d24134d 100644 --- a/src/Components/Components/src/CascadingValue.cs +++ b/src/Components/Components/src/CascadingValue.cs @@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.Components _hasSetParametersPreviously = true; - // It's OK for the value to be null, but some "Value" param must be suppled + // It's OK for the value to be null, but some "Value" param must be supplied // because it serves no useful purpose to have a <CascadingValue> otherwise. if (!hasSuppliedValue) { diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 51c95f386b..d4b38db345 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Components // about IComponent). This gives us flexibility to change the lifecycle concepts easily, // or for developers to design their own lifecycles as different base classes. - // TODO: When the component lifecycle design stabilises, add proper unit tests for ComponentBase. + // TODO: When the component lifecycle design stabilizes, add proper unit tests for ComponentBase. /// <summary> /// Optional base class for components. Alternatively, components may @@ -136,7 +136,7 @@ namespace Microsoft.AspNetCore.Components /// </param> /// <remarks> /// The <see cref="OnAfterRender(bool)"/> and <see cref="OnAfterRenderAsync(bool)"/> lifecycle methods - /// are useful for performing interop, or interacting with values recieved from <c>@ref</c>. + /// are useful for performing interop, or interacting with values received from <c>@ref</c>. /// Use the <paramref name="firstRender"/> parameter to ensure that initialization work is only performed /// once. /// </remarks> @@ -156,7 +156,7 @@ namespace Microsoft.AspNetCore.Components /// <returns>A <see cref="Task"/> representing any asynchronous operation.</returns> /// <remarks> /// The <see cref="OnAfterRender(bool)"/> and <see cref="OnAfterRenderAsync(bool)"/> lifecycle methods - /// are useful for performing interop, or interacting with values recieved from <c>@ref</c>. + /// are useful for performing interop, or interacting with values received from <c>@ref</c>. /// Use the <paramref name="firstRender"/> parameter to ensure that initialization work is only performed /// once. /// </remarks> @@ -246,7 +246,7 @@ namespace Microsoft.AspNetCore.Components } catch // avoiding exception filters for AOT runtime support { - // Ignore exceptions from task cancelletions. + // Ignore exceptions from task cancellations. // Awaiting a canceled task may produce either an OperationCanceledException (if produced as a consequence of // CancellationToken.ThrowIfCancellationRequested()) or a TaskCanceledException (produced as a consequence of awaiting Task.FromCanceled). // It's much easier to check the state of the Task (i.e. Task.IsCanceled) rather than catch two distinct exceptions. @@ -289,7 +289,7 @@ namespace Microsoft.AspNetCore.Components } catch // avoiding exception filters for AOT runtime support { - // Ignore exceptions from task cancelletions, but don't bother issuing a state change. + // Ignore exceptions from task cancellations, but don't bother issuing a state change. if (task.IsCanceled) { return; diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs index 99eac1965a..2a27533dfb 100644 --- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs +++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs @@ -12,9 +12,9 @@ namespace Microsoft.AspNetCore.Components /// </summary> // // NOTE: for number parsing, the HTML5 spec dictates that <input type="number"> the DOM will represent - // number values as floating point numbers using `.` as the period separator. This is NOT culture senstive. + // number values as floating point numbers using `.` as the period separator. This is NOT culture sensitive. // Put another way, the user might see `,` as their decimal separator, but the value available in events - // to JS code is always simpilar to what .NET parses with InvariantCulture. + // to JS code is always similar to what .NET parses with InvariantCulture. // // See: https://www.w3.org/TR/html5/sec-forms.html#number-state-typenumber // See: https://www.w3.org/TR/html5/infrastructure.html#valid-floating-point-number diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec b/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec index 3c4fee8be5..b7c428d87e 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec @@ -9,7 +9,7 @@ <dependency id="Microsoft.JSInterop" version="$jsInteropPackageVersion$" exclude="Build,Analyzers" /> <dependency id="System.ComponentModel.Annotations" version="$systemComponentModelAnnotationsPackageVersion$" exclude="Build,Analyzers" /> </group> - <group targetFramework=".NETCoreApp3.1"> + <group targetFramework=".NETCoreApp5.0"> <dependency id="Microsoft.AspNetCore.Components.Analyzers" version="$componentAnalyzerPackageVersion$" /> <dependency id="Microsoft.AspNetCore.Authorization" version="$authorizationPackageVersion$" exclude="Build,Analyzers" /> <dependency id="Microsoft.JSInterop" version="$jsInteropPackageVersion$" exclude="Build,Analyzers" /> diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec b/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec index 80f61e7f17..8e45bbdac0 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec @@ -3,7 +3,7 @@ <metadata> $CommonMetadataElements$ <dependencies> - <group targetFramework=".NETCoreApp3.1"> + <group targetFramework=".NETCoreApp5.0"> <dependency id="Microsoft.AspNetCore.Components.Analyzers" version="$componentAnalyzerPackageVersion$" /> <dependency id="Microsoft.AspNetCore.Authorization" version="$authorizationPackageVersion$" exclude="Build,Analyzers" /> <dependency id="Microsoft.JSInterop" version="$jsInteropPackageVersion$" exclude="Build,Analyzers" /> diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index d75077026f..0ad565fb54 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components { /// <summary> - /// Provides an abstraction for querying and mananging URI navigation. + /// Provides an abstraction for querying and managing URI navigation. /// </summary> public abstract class NavigationManager { @@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Components } /// <summary> - /// Allows derived classes to lazyly self-initialize. Implementations that support lazy-initialization should override + /// Allows derived classes to lazily self-initialize. Implementations that support lazy-initialization should override /// this method and call <see cref="Initialize(string, string)" />. /// </summary> protected virtual void EnsureInitialized() diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs index e73119e038..1e6249c8d7 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree public enum RenderTreeFrameType: short { /// <summary> - /// Used only for unintialized frames. + /// Used only for uninitialized frames. /// </summary> None = 0, diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 05cfb41abe..09470cebda 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree // remaining work. // During the synchronous rendering process we don't wait for the pending asynchronous // work to finish as it will simply trigger new renders that will be handled afterwards. - // During the asynchronous rendering process we want to wait up untill al components have + // During the asynchronous rendering process we want to wait up until all components have // finished rendering so that we can produce the complete output. var componentState = GetRequiredComponentState(componentId); componentState.SetDirectParameters(initialParameters); @@ -388,7 +388,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree : null; /// <summary> - /// Processses pending renders requests from components if there are any. + /// Processes pending renders requests from components if there are any. /// </summary> protected virtual void ProcessPendingRender() { diff --git a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs index 6876c97f0d..d648cdf5bc 100644 --- a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs +++ b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs @@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Components.Rendering AssertCanAddAttribute(); if (_lastNonAttributeFrameType == RenderTreeFrameType.Component) { - // Since this is a component, we need to preserve the type of the EventCallabck, so we have + // Since this is a component, we need to preserve the type of the EventCallback, so we have // to box. Append(RenderTreeFrame.Attribute(sequence, name, (object)value)); } diff --git a/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs b/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs index 176e4d83e4..d25d50b6de 100644 --- a/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs +++ b/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs @@ -147,20 +147,20 @@ namespace Microsoft.AspNetCore.Components.Rendering // synchronously runs the callback public override void Send(SendOrPostCallback d, object state) { - Task antecedant; + Task antecedent; var completion = new TaskCompletionSource<object>(); lock (_state.Lock) { - antecedant = _state.Task; + antecedent = _state.Task; _state.Task = completion.Task; } // We have to block. That's the contract of Send - we don't expect this to be used // in many scenarios in Components. // - // Using Wait here is ok because the antecedant task will never throw. - antecedant.Wait(); + // Using Wait here is ok because the antecedent task will never throw. + antecedent.Wait(); ExecuteSynchronously(completion, d, state); } @@ -195,7 +195,7 @@ namespace Microsoft.AspNetCore.Components.Rendering ExecuteSynchronously(completion, d, state); } - private Task Enqueue(Task antecedant, SendOrPostCallback d, object state, bool forceAsync = false) + private Task Enqueue(Task antecedent, SendOrPostCallback d, object state, bool forceAsync = false) { // If we get here is means that a callback is being explicitly queued. Let's instead add it to the queue and yield. // @@ -212,7 +212,7 @@ namespace Microsoft.AspNetCore.Components.Rendering } var flags = forceAsync ? TaskContinuationOptions.RunContinuationsAsynchronously : TaskContinuationOptions.None; - return antecedant.ContinueWith(BackgroundWorkThunk, new WorkItem() + return antecedent.ContinueWith(BackgroundWorkThunk, new WorkItem() { SynchronizationContext = this, ExecutionContext = executionContext, diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index a906664741..522d027d6a 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -222,7 +222,7 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert 2: Re-render the CascadingValue; observe nested component wasn't re-rendered providedValue = "Updated value"; - displayNestedComponent = false; // Remove the nested componet + displayNestedComponent = false; // Remove the nested component component.TriggerRender(); // Assert: We did not render the nested component now it's been removed diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 9df9feab9f..5b682e56a3 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -366,7 +366,7 @@ namespace Microsoft.AspNetCore.Components public void HasDuplicateCaptureUnmatchedValuesParameters_Throws() { // Arrange - var target = new HasDupliateCaptureUnmatchedValuesProperty(); + var target = new HasDuplicateCaptureUnmatchedValuesProperty(); var parameters = new ParameterViewBuilder().Build(); // Act @@ -374,17 +374,17 @@ namespace Microsoft.AspNetCore.Components // Assert Assert.Equal( - $"Multiple properties were found on component type '{typeof(HasDupliateCaptureUnmatchedValuesProperty).FullName}' " + + $"Multiple properties were found on component type '{typeof(HasDuplicateCaptureUnmatchedValuesProperty).FullName}' " + $"with '{nameof(ParameterAttribute)}.{nameof(ParameterAttribute.CaptureUnmatchedValues)}'. " + $"Only a single property per type can use '{nameof(ParameterAttribute)}.{nameof(ParameterAttribute.CaptureUnmatchedValues)}'. " + $"Properties:" + Environment.NewLine + - $"{nameof(HasDupliateCaptureUnmatchedValuesProperty.CaptureUnmatchedValuesProp1)}" + Environment.NewLine + - $"{nameof(HasDupliateCaptureUnmatchedValuesProperty.CaptureUnmatchedValuesProp2)}", + $"{nameof(HasDuplicateCaptureUnmatchedValuesProperty.CaptureUnmatchedValuesProp1)}" + Environment.NewLine + + $"{nameof(HasDuplicateCaptureUnmatchedValuesProperty.CaptureUnmatchedValuesProp2)}", ex.Message); } [Fact] - public void HasCaptureUnmatchedValuesParameteterWithWrongType_Throws() + public void HasCaptureUnmatchedValuesParameterWithWrongType_Throws() { // Arrange var target = new HasWrongTypeCaptureUnmatchedValuesProperty(); @@ -630,7 +630,7 @@ namespace Microsoft.AspNetCore.Components [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> CaptureUnmatchedValues { get; set; } } - class HasDupliateCaptureUnmatchedValuesProperty + class HasDuplicateCaptureUnmatchedValuesProperty { [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> CaptureUnmatchedValuesProp1 { get; set; } [Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> CaptureUnmatchedValuesProp2 { get; set; } diff --git a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs index 28f617eba2..9f3ba71809 100644 --- a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs +++ b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs @@ -442,7 +442,7 @@ namespace Microsoft.AspNetCore.Components.Test [Fact] public void HandlesKeyBeingAdded() { - // This is an anomolous situation that can't occur with .razor components. + // This is an anomalous situation that can't occur with .razor components. // It represents the case where, for the same sequence number, we have an // old frame without a key and a new frame with a key. @@ -472,7 +472,7 @@ namespace Microsoft.AspNetCore.Components.Test [Fact] public void HandlesKeyBeingRemoved() { - // This is an anomolous situation that can't occur with .razor components. + // This is an anomalous situation that can't occur with .razor components. // It represents the case where, for the same sequence number, we have an // old frame with a key and a new frame without a key. diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index d0e2affea2..b2bd6d5d5e 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -3574,7 +3574,7 @@ namespace Microsoft.AspNetCore.Components.Test // Act &A Assert renderer.Dispose(); - // All components must be disposed even if some throw as part of being diposed. + // All components must be disposed even if some throw as part of being disposed. Assert.True(component.Disposed); var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions)); Assert.Contains(exception1, aex.InnerExceptions); diff --git a/src/Components/Components/test/Rendering/RendererSynchronizationContextTest.cs b/src/Components/Components/test/Rendering/RendererSynchronizationContextTest.cs index 568d2501bb..c92a585bd6 100644 --- a/src/Components/Components/test/Rendering/RendererSynchronizationContextTest.cs +++ b/src/Components/Components/test/Rendering/RendererSynchronizationContextTest.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Components.Rendering } [Fact] - public void Post_RunsAynchronously_WhenNotBusy_Exception() + public void Post_RunsAsynchronously_WhenNotBusy_Exception() { // Arrange var context = new RendererSynchronizationContext(); diff --git a/src/Components/Server/src/CircuitDisconnectMiddleware.cs b/src/Components/Server/src/CircuitDisconnectMiddleware.cs index d64c31e7da..ac5cf4c6cc 100644 --- a/src/Components/Server/src/CircuitDisconnectMiddleware.cs +++ b/src/Components/Server/src/CircuitDisconnectMiddleware.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Server { - // We use a middlware so that we can use DI. + // We use a middleware so that we can use DI. internal class CircuitDisconnectMiddleware { private const string CircuitIdKey = "circuitId"; diff --git a/src/Components/Server/src/CircuitOptions.cs b/src/Components/Server/src/CircuitOptions.cs index 68ca25c85a..9f862b069d 100644 --- a/src/Components/Server/src/CircuitOptions.cs +++ b/src/Components/Server/src/CircuitOptions.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Server /// without losing any state in the event of transient connection issues. /// </para> /// <para> - /// This value determines the maximium number of circuit states retained by the server. + /// This value determines the maximum number of circuit states retained by the server. /// <seealso cref="DisconnectedCircuitRetentionPeriod"/> /// </para> /// </summary> @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Components.Server /// without losing any state in the event of transient connection issues. /// </para> /// <para> - /// This value determines the maximium duration circuit state is retained by the server before being evicted. + /// This value determines the maximum duration circuit state is retained by the server before being evicted. /// <seealso cref="DisconnectedCircuitMaxRetained"/> /// </para> /// </summary> diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index bfb4b0cdae..a6a7850e81 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -355,7 +355,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits // EndInvokeJSFromDotNet is used in a fire-and-forget context, so it's responsible for its own // error handling. - public async Task EndInvokeJSFromDotNet(long asyncCall, bool succeded, string arguments) + public async Task EndInvokeJSFromDotNet(long asyncCall, bool succeeded, string arguments) { AssertInitialized(); AssertNotDisposed(); @@ -364,7 +364,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { await Renderer.Dispatcher.InvokeAsync(() => { - if (!succeded) + if (!succeeded) { // We can log the arguments here because it is simply the JS error with the call stack. Log.EndInvokeJSFailed(_logger, asyncCall, arguments); @@ -577,11 +577,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits private static class Log { - private static readonly Action<ILogger, Exception> _intializationStarted; - private static readonly Action<ILogger, Exception> _intializationSucceded; - private static readonly Action<ILogger, Exception> _intializationFailed; + private static readonly Action<ILogger, Exception> _initializationStarted; + private static readonly Action<ILogger, Exception> _initializationSucceded; + private static readonly Action<ILogger, Exception> _initializationFailed; private static readonly Action<ILogger, CircuitId, Exception> _disposeStarted; - private static readonly Action<ILogger, CircuitId, Exception> _disposeSucceded; + private static readonly Action<ILogger, CircuitId, Exception> _disposeSucceeded; private static readonly Action<ILogger, CircuitId, Exception> _disposeFailed; private static readonly Action<ILogger, CircuitId, Exception> _onCircuitOpened; private static readonly Action<ILogger, CircuitId, string, Exception> _onConnectionUp; @@ -639,7 +639,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public static readonly EventId EndInvokeJSSucceeded = new EventId(206, "EndInvokeJSSucceeded"); public static readonly EventId DispatchEventThroughJSInterop = new EventId(207, "DispatchEventThroughJSInterop"); public static readonly EventId LocationChange = new EventId(208, "LocationChange"); - public static readonly EventId LocationChangeSucceded = new EventId(209, "LocationChangeSucceeded"); + public static readonly EventId LocationChangeSucceeded = new EventId(209, "LocationChangeSucceeded"); public static readonly EventId LocationChangeFailed = new EventId(210, "LocationChangeFailed"); public static readonly EventId LocationChangeFailedInCircuit = new EventId(211, "LocationChangeFailedInCircuit"); public static readonly EventId OnRenderCompletedFailed = new EventId(212, "OnRenderCompletedFailed"); @@ -647,17 +647,17 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits static Log() { - _intializationStarted = LoggerMessage.Define( + _initializationStarted = LoggerMessage.Define( LogLevel.Debug, EventIds.InitializationStarted, "Circuit initialization started."); - _intializationSucceded = LoggerMessage.Define( + _initializationSucceded = LoggerMessage.Define( LogLevel.Debug, EventIds.InitializationSucceeded, "Circuit initialization succeeded."); - _intializationFailed = LoggerMessage.Define( + _initializationFailed = LoggerMessage.Define( LogLevel.Debug, EventIds.InitializationFailed, "Circuit initialization failed."); @@ -667,10 +667,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits EventIds.DisposeStarted, "Disposing circuit '{CircuitId}' started."); - _disposeSucceded = LoggerMessage.Define<CircuitId>( + _disposeSucceeded = LoggerMessage.Define<CircuitId>( LogLevel.Debug, EventIds.DisposeSucceeded, - "Disposing circuit '{CircuitId}' succeded."); + "Disposing circuit '{CircuitId}' succeeded."); _disposeFailed = LoggerMessage.Define<CircuitId>( LogLevel.Debug, @@ -725,7 +725,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits _unhandledExceptionClientDisconnected = LoggerMessage.Define<CircuitId>( LogLevel.Debug, EventIds.UnhandledExceptionClientDisconnected, - "An exception ocurred on the circuit host '{CircuitId}' while the client is disconnected."); + "An exception occurred on the circuit host '{CircuitId}' while the client is disconnected."); _beginInvokeDotNetStatic = LoggerMessage.Define<string, string, string>( LogLevel.Debug, @@ -779,8 +779,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits _locationChangeSucceeded = LoggerMessage.Define<string, CircuitId>( LogLevel.Debug, - EventIds.LocationChangeSucceded, - "Location change to '{URI}' in circuit '{CircuitId}' succeded."); + EventIds.LocationChangeSucceeded, + "Location change to '{URI}' in circuit '{CircuitId}' succeeded."); _locationChangeFailed = LoggerMessage.Define<string, CircuitId>( LogLevel.Debug, @@ -798,11 +798,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits "Failed to complete render batch '{RenderId}' in circuit host '{CircuitId}'."); } - public static void InitializationStarted(ILogger logger) => _intializationStarted(logger, null); - public static void InitializationSucceeded(ILogger logger) => _intializationSucceded(logger, null); - public static void InitializationFailed(ILogger logger, Exception exception) => _intializationFailed(logger, exception); + public static void InitializationStarted(ILogger logger) => _initializationStarted(logger, null); + public static void InitializationSucceeded(ILogger logger) => _initializationSucceded(logger, null); + public static void InitializationFailed(ILogger logger, Exception exception) => _initializationFailed(logger, exception); public static void DisposeStarted(ILogger logger, CircuitId circuitId) => _disposeStarted(logger, circuitId, null); - public static void DisposeSucceeded(ILogger logger, CircuitId circuitId) => _disposeSucceded(logger, circuitId, null); + public static void DisposeSucceeded(ILogger logger, CircuitId circuitId) => _disposeSucceeded(logger, circuitId, null); public static void DisposeFailed(ILogger logger, CircuitId circuitId, Exception exception) => _disposeFailed(logger, circuitId, exception); public static void CircuitOpened(ILogger logger, CircuitId circuitId) => _onCircuitOpened(logger, circuitId, null); public static void ConnectionUp(ILogger logger, CircuitId circuitId, string connectionId) => _onConnectionUp(logger, circuitId, connectionId, null); diff --git a/src/Components/Server/src/Circuits/CircuitRegistry.cs b/src/Components/Server/src/Circuits/CircuitRegistry.cs index ab261a108b..f8e4971a64 100644 --- a/src/Components/Server/src/Circuits/CircuitRegistry.cs +++ b/src/Components/Server/src/Circuits/CircuitRegistry.cs @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits /// the <see cref="CircuitClientProxy"/> to use the new client instance that attempted to reconnect to the server. Removing the entry from /// <see cref="DisconnectedCircuits"/> should ensure we no longer have to concern ourselves with entry expiration. /// - /// Knowing when a client disconnected is not an exact science. There's a fair possiblity that a client may reconnect before the server realizes. + /// Knowing when a client disconnected is not an exact science. There's a fair possibility that a client may reconnect before the server realizes. /// Consequently, we have to account for reconnects and disconnects occuring simultaneously as well as appearing out of order. /// To manage this, we use a critical section to manage all state transitions. /// </remarks> @@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits else { // DisconnectCore may fail to disconnect the circuit if it was previously marked inactive or - // has been transfered to a new connection. Do not invoke the circuit handlers in this instance. + // has been transferred to a new connection. Do not invoke the circuit handlers in this instance. // We have to do in this instance. return Task.CompletedTask; @@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { // Transition the host from disconnected to connected if it's available. In this critical section, we return // an existing host if it's currently considered connected or transition a disconnected host to connected. - // Transfering also wires up the client to the new set. + // Transferring also wires up the client to the new set. (circuitHost, previouslyConnected) = ConnectCore(circuitId, clientProxy, connectionId); if (circuitHost == null) @@ -428,7 +428,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits _connectingToDisconnectedCircuit = LoggerMessage.Define<CircuitId, string>( LogLevel.Debug, EventIds.ConnectingToDisconnectedCircuit, - "Transfering disconnected circuit {CircuitId} to connection {ConnectionId}."); + "Transferring disconnected circuit {CircuitId} to connection {ConnectionId}."); _failedToReconnectToCircuit = LoggerMessage.Define<CircuitId>( LogLevel.Debug, diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 509944e17a..063fa77965 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -90,14 +90,14 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits // as we have a client that is not acknowledging render batches fast enough (something we consider needs // to be fast). // The result is something as follows: - // Lets imagine an extreme case where the server produces a new batch every milisecond. - // Lets say the client is able to ACK a batch every 100 miliseconds. + // Lets imagine an extreme case where the server produces a new batch every millisecond. + // Lets say the client is able to ACK a batch every 100 milliseconds. // When the app starts the client might see the sequence 0->(MaxUnacknowledgedRenderBatches-1) and then - // after 100 miliseconds it sees it jump to 1xx, then to 2xx where xx is something between {0..99} the + // after 100 milliseconds it sees it jump to 1xx, then to 2xx where xx is something between {0..99} the // reason for this is that the server slows down rendering new batches to as fast as the client can consume // them. // Similarly, if a client were to send events at a faster pace than the server can consume them, the server - // would still proces the events, but would not produce new renders until it gets an ack that frees up space + // would still process the events, but would not produce new renders until it gets an ack that frees up space // for a new render. // We should never see UnacknowledgedRenderBatches.Count > _options.MaxBufferedUnacknowledgedRenderBatches @@ -202,7 +202,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { // Send the render batch to the client // If the "send" operation fails (synchronously or asynchronously) or the client - // gets disconected simply give up. This likely means that + // gets disconnected simply give up. This likely means that // the circuit went offline while sending the data, so simply wait until the // client reconnects back or the circuit gets evicted because it stayed // disconnected for too long. @@ -247,7 +247,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits // from the client that it has received and successfully applied all batches up to that point). // If receive an ack for a previously acknowledged batch, its an error, as the messages are - // guranteed to be delivered in order, so a message for a render batch of 2 will never arrive + // guaranteed to be delivered in order, so a message for a render batch of 2 will never arrive // after a message for a render batch for 3. // If that were to be the case, it would just be enough to relax the checks here and simply skip // the message. @@ -282,7 +282,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits if (lastBatchId < incomingBatchId) { - // This exception is due to a bad client input, so we mark it as such to prevent loging it as a warning and + // This exception is due to a bad client input, so we mark it as such to prevent logging it as a warning and // flooding the logs with warnings. throw new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'."); } diff --git a/src/Components/Server/test/Circuits/CircuitIdFactoryTest.cs b/src/Components/Server/test/Circuits/CircuitIdFactoryTest.cs index 35bc454416..588eadfdd8 100644 --- a/src/Components/Server/test/Circuits/CircuitIdFactoryTest.cs +++ b/src/Components/Server/test/Circuits/CircuitIdFactoryTest.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } [Fact] - public void CreateCircuitId_Generates_GeneratesDifferentIds_ForSuccesiveCalls() + public void CreateCircuitId_Generates_GeneratesDifferentIds_ForSuccessiveCalls() { // Arrange var factory = TestCircuitIdFactory.CreateTestFactory(); diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index e7cda62bb2..6befe0c65d 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -179,7 +179,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // Assert Assert.Equal(new long[] { 2, 3, 4 }, renderIds); - Assert.True(task.Wait(3000), "One or more render batches werent acknowledged"); + Assert.True(task.Wait(3000), "One or more render batches weren't acknowledged"); await task; } @@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering exceptions.Add(e); }; - // Receive the ack for the intial batch + // Receive the ack for the initial batch _ = renderer.OnRenderCompletedAsync(2, null); // Receive the ack for the second batch _ = renderer.OnRenderCompletedAsync(3, null); diff --git a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs index e0a6d8ff44..03a651d7be 100644 --- a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs +++ b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs @@ -66,9 +66,9 @@ namespace Microsoft.AspNetCore.Components.Server.Tests services.AddServerSideBlazor(); services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build()); - var serviceProvder = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - return new ApplicationBuilder(serviceProvder); + return new ApplicationBuilder(serviceProvider); } private class MyComponent : IComponent diff --git a/src/Components/Shared/src/ElementReferenceJsonConverter.cs b/src/Components/Shared/src/ElementReferenceJsonConverter.cs index 465a688bf2..80a7738107 100644 --- a/src/Components/Shared/src/ElementReferenceJsonConverter.cs +++ b/src/Components/Shared/src/ElementReferenceJsonConverter.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Components } else { - throw new JsonException($"Unexcepted JSON Token {reader.TokenType}."); + throw new JsonException($"Unexpected JSON Token {reader.TokenType}."); } } @@ -49,4 +49,4 @@ namespace Microsoft.AspNetCore.Components writer.WriteEndObject(); } } -}
\ No newline at end of file +} diff --git a/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj b/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj index 8e0a17ece0..cb890a62b8 100644 --- a/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj +++ b/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj @@ -7,6 +7,11 @@ </PropertyGroup> <ItemGroup> + <BuildOutputFiles Include="dist\release\blazor.server.js" /> + <BuildOutputFiles Include="dist\release\blazor.webassembly.js" /> + </ItemGroup> + + <ItemGroup> <ProjectReference Include="..\..\SignalR\clients\ts\signalr\signalr.npmproj" ReferenceOutputAssemblies="false" diff --git a/src/Components/Web.JS/dist/Release/blazor.server.js b/src/Components/Web.JS/dist/Release/blazor.server.js index 65fbad86e0..9a47cfbee7 100644 --- a/src/Components/Web.JS/dist/Release/blazor.server.js +++ b/src/Components/Web.JS/dist/Release/blazor.server.js @@ -12,4 +12,4 @@ var r=n(50),o=n(51),i=n(52);function a(){return c.TYPED_ARRAY_SUPPORT?2147483647 * @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org> * @license MIT */ -function r(e,t){if(e===t)return 0;for(var n=e.length,r=t.length,o=0,i=Math.min(n,r);o<i;++o)if(e[o]!==t[o]){n=e[o],r=t[o];break}return n<r?-1:r<n?1:0}function o(e){return t.Buffer&&"function"==typeof t.Buffer.isBuffer?t.Buffer.isBuffer(e):!(null==e||!e._isBuffer)}var i=n(35),a=Object.prototype.hasOwnProperty,s=Array.prototype.slice,c="foo"===function(){}.name;function u(e){return Object.prototype.toString.call(e)}function l(e){return!o(e)&&("function"==typeof t.ArrayBuffer&&("function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):!!e&&(e instanceof DataView||!!(e.buffer&&e.buffer instanceof ArrayBuffer))))}var f=e.exports=v,h=/\s*function\s+([^\(\s]*)\s*/;function p(e){if(i.isFunction(e)){if(c)return e.name;var t=e.toString().match(h);return t&&t[1]}}function d(e,t){return"string"==typeof e?e.length<t?e:e.slice(0,t):e}function g(e){if(c||!i.isFunction(e))return i.inspect(e);var t=p(e);return"[Function"+(t?": "+t:"")+"]"}function y(e,t,n,r,o){throw new f.AssertionError({message:n,actual:e,expected:t,operator:r,stackStartFunction:o})}function v(e,t){e||y(e,!0,t,"==",f.ok)}function b(e,t,n,a){if(e===t)return!0;if(o(e)&&o(t))return 0===r(e,t);if(i.isDate(e)&&i.isDate(t))return e.getTime()===t.getTime();if(i.isRegExp(e)&&i.isRegExp(t))return e.source===t.source&&e.global===t.global&&e.multiline===t.multiline&&e.lastIndex===t.lastIndex&&e.ignoreCase===t.ignoreCase;if(null!==e&&"object"==typeof e||null!==t&&"object"==typeof t){if(l(e)&&l(t)&&u(e)===u(t)&&!(e instanceof Float32Array||e instanceof Float64Array))return 0===r(new Uint8Array(e.buffer),new Uint8Array(t.buffer));if(o(e)!==o(t))return!1;var c=(a=a||{actual:[],expected:[]}).actual.indexOf(e);return-1!==c&&c===a.expected.indexOf(t)||(a.actual.push(e),a.expected.push(t),function(e,t,n,r){if(null==e||null==t)return!1;if(i.isPrimitive(e)||i.isPrimitive(t))return e===t;if(n&&Object.getPrototypeOf(e)!==Object.getPrototypeOf(t))return!1;var o=m(e),a=m(t);if(o&&!a||!o&&a)return!1;if(o)return e=s.call(e),t=s.call(t),b(e,t,n);var c,u,l=S(e),f=S(t);if(l.length!==f.length)return!1;for(l.sort(),f.sort(),u=l.length-1;u>=0;u--)if(l[u]!==f[u])return!1;for(u=l.length-1;u>=0;u--)if(c=l[u],!b(e[c],t[c],n,r))return!1;return!0}(e,t,n,a))}return n?e===t:e==t}function m(e){return"[object Arguments]"==Object.prototype.toString.call(e)}function w(e,t){if(!e||!t)return!1;if("[object RegExp]"==Object.prototype.toString.call(t))return t.test(e);try{if(e instanceof t)return!0}catch(e){}return!Error.isPrototypeOf(t)&&!0===t.call({},e)}function E(e,t,n,r){var o;if("function"!=typeof t)throw new TypeError('"block" argument must be a function');"string"==typeof n&&(r=n,n=null),o=function(e){var t;try{e()}catch(e){t=e}return t}(t),r=(n&&n.name?" ("+n.name+").":".")+(r?" "+r:"."),e&&!o&&y(o,n,"Missing expected exception"+r);var a="string"==typeof r,s=!e&&o&&!n;if((!e&&i.isError(o)&&a&&w(o,n)||s)&&y(o,n,"Got unwanted exception"+r),e&&o&&n&&!w(o,n)||!e&&o)throw o}f.AssertionError=function(e){var t;this.name="AssertionError",this.actual=e.actual,this.expected=e.expected,this.operator=e.operator,e.message?(this.message=e.message,this.generatedMessage=!1):(this.message=d(g((t=this).actual),128)+" "+t.operator+" "+d(g(t.expected),128),this.generatedMessage=!0);var n=e.stackStartFunction||y;if(Error.captureStackTrace)Error.captureStackTrace(this,n);else{var r=new Error;if(r.stack){var o=r.stack,i=p(n),a=o.indexOf("\n"+i);if(a>=0){var s=o.indexOf("\n",a+1);o=o.substring(s+1)}this.stack=o}}},i.inherits(f.AssertionError,Error),f.fail=y,f.ok=v,f.equal=function(e,t,n){e!=t&&y(e,t,n,"==",f.equal)},f.notEqual=function(e,t,n){e==t&&y(e,t,n,"!=",f.notEqual)},f.deepEqual=function(e,t,n){b(e,t,!1)||y(e,t,n,"deepEqual",f.deepEqual)},f.deepStrictEqual=function(e,t,n){b(e,t,!0)||y(e,t,n,"deepStrictEqual",f.deepStrictEqual)},f.notDeepEqual=function(e,t,n){b(e,t,!1)&&y(e,t,n,"notDeepEqual",f.notDeepEqual)},f.notDeepStrictEqual=function e(t,n,r){b(t,n,!0)&&y(t,n,r,"notDeepStrictEqual",e)},f.strictEqual=function(e,t,n){e!==t&&y(e,t,n,"===",f.strictEqual)},f.notStrictEqual=function(e,t,n){e===t&&y(e,t,n,"!==",f.notStrictEqual)},f.throws=function(e,t,n){E(!0,e,t,n)},f.doesNotThrow=function(e,t,n){E(!1,e,t,n)},f.ifError=function(e){if(e)throw e};var S=Object.keys||function(e){var t=[];for(var n in e)a.call(e,n)&&t.push(n);return t}}).call(this,n(10))},function(e,t){e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){e.exports=n(11)},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t){},function(e,t,n){"use strict";var r=n(15).Buffer,o=n(60);e.exports=function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.head=null,this.tail=null,this.length=0}return e.prototype.push=function(e){var t={data:e,next:null};this.length>0?this.tail.next=t:this.head=t,this.tail=t,++this.length},e.prototype.unshift=function(e){var t={data:e,next:this.head};0===this.length&&(this.tail=t),this.head=t,++this.length},e.prototype.shift=function(){if(0!==this.length){var e=this.head.data;return 1===this.length?this.head=this.tail=null:this.head=this.head.next,--this.length,e}},e.prototype.clear=function(){this.head=this.tail=null,this.length=0},e.prototype.join=function(e){if(0===this.length)return"";for(var t=this.head,n=""+t.data;t=t.next;)n+=e+t.data;return n},e.prototype.concat=function(e){if(0===this.length)return r.alloc(0);if(1===this.length)return this.head.data;for(var t,n,o,i=r.allocUnsafe(e>>>0),a=this.head,s=0;a;)t=a.data,n=i,o=s,t.copy(n,o),s+=a.data.length,a=a.next;return i},e}(),o&&o.inspect&&o.inspect.custom&&(e.exports.prototype[o.inspect.custom]=function(){var e=o.inspect({length:this.length});return this.constructor.name+" "+e})},function(e,t){},function(e,t,n){var r=n(6),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=a),i(o,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){(function(e){var r=void 0!==e&&e||"undefined"!=typeof self&&self||window,o=Function.prototype.apply;function i(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new i(o.call(setTimeout,r,arguments),clearTimeout)},t.setInterval=function(){return new i(o.call(setInterval,r,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(r,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(63),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(10))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var r,o,i,a,s,c=1,u={},l=!1,f=e.document,h=Object.getPrototypeOf&&Object.getPrototypeOf(e);h=h&&h.setTimeout?h:e,"[object process]"==={}.toString.call(e.process)?r=function(e){t.nextTick(function(){d(e)})}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((i=new MessageChannel).port1.onmessage=function(e){d(e.data)},r=function(e){i.port2.postMessage(e)}):f&&"onreadystatechange"in f.createElement("script")?(o=f.documentElement,r=function(e){var t=f.createElement("script");t.onreadystatechange=function(){d(e),t.onreadystatechange=null,o.removeChild(t),t=null},o.appendChild(t)}):r=function(e){setTimeout(d,0,e)}:(a="setImmediate$"+Math.random()+"$",s=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(a)&&d(+t.data.slice(a.length))},e.addEventListener?e.addEventListener("message",s,!1):e.attachEvent("onmessage",s),r=function(t){e.postMessage(a+t,"*")}),h.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n<t.length;n++)t[n]=arguments[n+1];var o={callback:e,args:t};return u[c]=o,r(c),c++},h.clearImmediate=p}function p(e){delete u[e]}function d(e){if(l)setTimeout(d,0,e);else{var t=u[e];if(t){l=!0;try{!function(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}(t)}finally{p(e),l=!1}}}}}("undefined"==typeof self?void 0===e?this:e:self)}).call(this,n(10),n(20))},function(e,t,n){(function(t){function n(e){try{if(!t.localStorage)return!1}catch(e){return!1}var n=t.localStorage[e];return null!=n&&"true"===String(n).toLowerCase()}e.exports=function(e,t){if(n("noDeprecation"))return e;var r=!1;return function(){if(!r){if(n("throwDeprecation"))throw new Error(t);n("traceDeprecation")?console.trace(t):console.warn(t),r=!0}return e.apply(this,arguments)}}}).call(this,n(10))},function(e,t,n){"use strict";var r=n(66).Transform,o=n(16),i=n(22);function a(e){(e=e||{}).objectMode=!0,e.highWaterMark=16,r.call(this,e),this._msgpack=e.msgpack}function s(e){if(!(this instanceof s))return(e=e||{}).msgpack=this,new s(e);a.call(this,e)}function c(e){if(!(this instanceof c))return(e=e||{}).msgpack=this,new c(e);a.call(this,e),this._chunks=i()}o(a,r),o(s,a),s.prototype._transform=function(e,t,n){var r=null;try{r=this._msgpack.encode(e).slice(0)}catch(e){return this.emit("error",e),n()}this.push(r),n()},o(c,a),c.prototype._transform=function(e,t,n){e&&this._chunks.append(e);try{var r=this._msgpack.decode(this._chunks);this.push(r)}catch(e){return void(e instanceof this._msgpack.IncompleteBufferError?n():this.emit("error",e))}this._chunks.length>0?this._transform(null,t,n):n()},e.exports.decoder=c,e.exports.encoder=s},function(e,t,n){(t=e.exports=n(36)).Stream=t,t.Readable=t,t.Writable=n(41),t.Duplex=n(11),t.Transform=n(42),t.PassThrough=n(67)},function(e,t,n){"use strict";e.exports=i;var r=n(42),o=n(21);function i(e){if(!(this instanceof i))return new i(e);r.call(this,e)}o.inherits=n(16),o.inherits(i,r),i.prototype._transform=function(e,t,n){n(null,e)}},function(e,t,n){var r=n(22);function o(e){Error.call(this),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.name=this.constructor.name,this.message=e||"unable to decode"}n(35).inherits(o,Error),e.exports=function(e){return function(e){e instanceof r||(e=r().append(e));var t=i(e);if(t)return e.consume(t.bytesConsumed),t.value;throw new o};function t(e,t,n){return t>=n+e}function n(e,t){return{value:e,bytesConsumed:t}}function i(e,r){r=void 0===r?0:r;var o=e.length-r;if(o<=0)return null;var i,l,f,h=e.readUInt8(r),p=0;if(!function(e,t){var n=function(e){switch(e){case 196:return 2;case 197:return 3;case 198:return 5;case 199:return 3;case 200:return 4;case 201:return 6;case 202:return 5;case 203:return 9;case 204:return 2;case 205:return 3;case 206:return 5;case 207:return 9;case 208:return 2;case 209:return 3;case 210:return 5;case 211:return 9;case 212:return 3;case 213:return 4;case 214:return 6;case 215:return 10;case 216:return 18;case 217:return 2;case 218:return 3;case 219:return 5;case 222:return 3;default:return-1}}(e);return!(-1!==n&&t<n)}(h,o))return null;switch(h){case 192:return n(null,1);case 194:return n(!1,1);case 195:return n(!0,1);case 204:return n(p=e.readUInt8(r+1),2);case 205:return n(p=e.readUInt16BE(r+1),3);case 206:return n(p=e.readUInt32BE(r+1),5);case 207:for(f=7;f>=0;f--)p+=e.readUInt8(r+f+1)*Math.pow(2,8*(7-f));return n(p,9);case 208:return n(p=e.readInt8(r+1),2);case 209:return n(p=e.readInt16BE(r+1),3);case 210:return n(p=e.readInt32BE(r+1),5);case 211:return n(p=function(e,t){var n=128==(128&e[t]);if(n)for(var r=1,o=t+7;o>=t;o--){var i=(255^e[o])+r;e[o]=255&i,r=i>>8}var a=e.readUInt32BE(t+0),s=e.readUInt32BE(t+4);return(4294967296*a+s)*(n?-1:1)}(e.slice(r+1,r+9),0),9);case 202:return n(p=e.readFloatBE(r+1),5);case 203:return n(p=e.readDoubleBE(r+1),9);case 217:return t(i=e.readUInt8(r+1),o,2)?n(p=e.toString("utf8",r+2,r+2+i),2+i):null;case 218:return t(i=e.readUInt16BE(r+1),o,3)?n(p=e.toString("utf8",r+3,r+3+i),3+i):null;case 219:return t(i=e.readUInt32BE(r+1),o,5)?n(p=e.toString("utf8",r+5,r+5+i),5+i):null;case 196:return t(i=e.readUInt8(r+1),o,2)?n(p=e.slice(r+2,r+2+i),2+i):null;case 197:return t(i=e.readUInt16BE(r+1),o,3)?n(p=e.slice(r+3,r+3+i),3+i):null;case 198:return t(i=e.readUInt32BE(r+1),o,5)?n(p=e.slice(r+5,r+5+i),5+i):null;case 220:return o<3?null:(i=e.readUInt16BE(r+1),a(e,r,i,3));case 221:return o<5?null:(i=e.readUInt32BE(r+1),a(e,r,i,5));case 222:return i=e.readUInt16BE(r+1),s(e,r,i,3);case 223:throw new Error("map too big to decode in JS");case 212:return c(e,r,1);case 213:return c(e,r,2);case 214:return c(e,r,4);case 215:return c(e,r,8);case 216:return c(e,r,16);case 199:return i=e.readUInt8(r+1),l=e.readUInt8(r+2),t(i,o,3)?u(e,r,l,i,3):null;case 200:return i=e.readUInt16BE(r+1),l=e.readUInt8(r+3),t(i,o,4)?u(e,r,l,i,4):null;case 201:return i=e.readUInt32BE(r+1),l=e.readUInt8(r+5),t(i,o,6)?u(e,r,l,i,6):null}if(144==(240&h))return a(e,r,i=15&h,1);if(128==(240&h))return s(e,r,i=15&h,1);if(160==(224&h))return t(i=31&h,o,1)?n(p=e.toString("utf8",r+1,r+i+1),i+1):null;if(h>=224)return n(p=h-256,1);if(h<128)return n(h,1);throw new Error("not implemented yet")}function a(e,t,r,o){var a,s=[],c=0;for(t+=o,a=0;a<r;a++){var u=i(e,t);if(!u)return null;s.push(u.value),t+=u.bytesConsumed,c+=u.bytesConsumed}return n(s,o+c)}function s(e,t,r,o){var a,s={},c=0;for(t+=o,a=0;a<r;a++){var u=i(e,t);if(!u)return null;var l=i(e,t+=u.bytesConsumed);if(!l)return null;s[u.value]=l.value,t+=l.bytesConsumed,c+=u.bytesConsumed+l.bytesConsumed}return n(s,o+c)}function c(e,t,n){var r=e.readInt8(t+1);return u(e,t,r,n,2)}function u(t,r,o,i,a){var s,c;if(r+=a,o<0)switch(o){case-1:return function(e,t,r){var o,i;switch(i=0,t){case 4:o=e.readUInt32BE(0);break;case 8:var a=e.readUInt32BE(0),s=e.readUInt32BE(4);i=a/4,o=(3&a)*Math.pow(2,32)+s;break;case 12:throw new Error("timestamp 96 is not yet implemented")}var c=1e3*o+Math.round(i/1e6);return n(new Date(c),t+r)}(c=t.slice(r,r+i),i,a)}for(s=0;s<e.length;s++){if(o===e[s].type)return c=t.slice(r,r+i),n(e[s].decode(c),a+i)}throw new Error("unable to find ext type "+o)}},e.exports.IncompleteBufferError=o},function(e,t,n){"use strict";var r=n(15).Buffer,o=n(22),i=.1;function a(e,t){var n;return(n=r.allocUnsafe(5))[0]=202,n.writeFloatBE(e,1),(t||Math.abs(e-n.readFloatBE(1))>i)&&((n=r.allocUnsafe(9))[0]=203,n.writeDoubleBE(e,1)),n}e.exports=function(e,t,n,i){function s(c,u){var l,f,h;if(void 0===c)throw new Error("undefined is not encodable in msgpack!");if(null===c)(l=r.allocUnsafe(1))[0]=192;else if(!0===c)(l=r.allocUnsafe(1))[0]=195;else if(!1===c)(l=r.allocUnsafe(1))[0]=194;else if("string"==typeof c)(f=r.byteLength(c))<32?((l=r.allocUnsafe(1+f))[0]=160|f,f>0&&l.write(c,1)):f<=255&&!n?((l=r.allocUnsafe(2+f))[0]=217,l[1]=f,l.write(c,2)):f<=65535?((l=r.allocUnsafe(3+f))[0]=218,l.writeUInt16BE(f,1),l.write(c,3)):((l=r.allocUnsafe(5+f))[0]=219,l.writeUInt32BE(f,1),l.write(c,5));else if(c&&(c.readUInt32LE||c instanceof Uint8Array))c instanceof Uint8Array&&(c=r.from(c)),c.length<=255?((l=r.allocUnsafe(2))[0]=196,l[1]=c.length):c.length<=65535?((l=r.allocUnsafe(3))[0]=197,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=198,l.writeUInt32BE(c.length,1)),l=o([l,c]);else if(Array.isArray(c))c.length<16?(l=r.allocUnsafe(1))[0]=144|c.length:c.length<65536?((l=r.allocUnsafe(3))[0]=220,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=221,l.writeUInt32BE(c.length,1)),l=c.reduce(function(e,t){return e.append(s(t,!0)),e},o().append(l));else{if(!i&&"function"==typeof c.getDate)return function(e){var t,n=1*e,i=Math.floor(n/1e3),a=1e6*(n-1e3*i);if(a||i>4294967295){(t=new r(10))[0]=215,t[1]=-1;var s=4*a,c=i/Math.pow(2,32),u=s+c&4294967295,l=4294967295&i;t.writeInt32BE(u,2),t.writeInt32BE(l,6)}else(t=new r(6))[0]=214,t[1]=-1,t.writeUInt32BE(Math.floor(n/1e3),2);return o().append(t)}(c);if("object"==typeof c)l=function(t){var n,i,a=-1,s=[];for(n=0;n<e.length;n++)if(e[n].check(t)){i=e[n].encode(t);break}if(!i)return null;1==(a=i.length-1)?s.push(212):2===a?s.push(213):4===a?s.push(214):8===a?s.push(215):16===a?s.push(216):a<256?(s.push(199),s.push(a)):a<65536?(s.push(200),s.push(a>>8),s.push(255&a)):(s.push(201),s.push(a>>24),s.push(a>>16&255),s.push(a>>8&255),s.push(255&a));return o().append(r.from(s)).append(i)}(c)||function(e){var t,n,i=[],a=0;for(t in e)e.hasOwnProperty(t)&&void 0!==e[t]&&"function"!=typeof e[t]&&(++a,i.push(s(t,!0)),i.push(s(e[t],!0)));a<16?(n=r.allocUnsafe(1))[0]=128|a:((n=r.allocUnsafe(3))[0]=222,n.writeUInt16BE(a,1));return i.unshift(n),i.reduce(function(e,t){return e.append(t)},o())}(c);else if("number"==typeof c){if((h=c)!==Math.floor(h))return a(c,t);if(c>=0)if(c<128)(l=r.allocUnsafe(1))[0]=c;else if(c<256)(l=r.allocUnsafe(2))[0]=204,l[1]=c;else if(c<65536)(l=r.allocUnsafe(3))[0]=205,l.writeUInt16BE(c,1);else if(c<=4294967295)(l=r.allocUnsafe(5))[0]=206,l.writeUInt32BE(c,1);else{if(!(c<=9007199254740991))return a(c,!0);(l=r.allocUnsafe(9))[0]=207,function(e,t){for(var n=7;n>=0;n--)e[n+1]=255&t,t/=256}(l,c)}else if(c>=-32)(l=r.allocUnsafe(1))[0]=256+c;else if(c>=-128)(l=r.allocUnsafe(2))[0]=208,l.writeInt8(c,1);else if(c>=-32768)(l=r.allocUnsafe(3))[0]=209,l.writeInt16BE(c,1);else if(c>-214748365)(l=r.allocUnsafe(5))[0]=210,l.writeInt32BE(c,1);else{if(!(c>=-9007199254740991))return a(c,!0);(l=r.allocUnsafe(9))[0]=211,function(e,t,n){var r=n<0;r&&(n=Math.abs(n));var o=n%4294967296,i=n/4294967296;if(e.writeUInt32BE(Math.floor(i),t+0),e.writeUInt32BE(o,t+4),r)for(var a=1,s=t+7;s>=t;s--){var c=(255^e[s])+a;e[s]=255&c,a=c>>8}}(l,1,c)}}}if(!l)throw new Error("not implemented yet");return u?l:l.slice()}return s}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var i=n(5),a=n(71),s=n(17),c=function(){function e(e,t){this.nextBatchId=2,this.browserRendererId=e,this.logger=t}return e.getOrCreate=function(t){return e.instance||(e.instance=new e(0,t)),this.instance},e.prototype.processBatch=function(e,t,n){return r(this,void 0,void 0,function(){var r;return o(this,function(o){switch(o.label){case 0:return e<this.nextBatchId?[4,this.completeBatch(n,e)]:[3,2];case 1:return o.sent(),this.logger.log(s.LogLevel.Debug,"Batch "+e+" already processed. Waiting for batch "+this.nextBatchId+"."),[2];case 2:return e>this.nextBatchId?this.fatalError?(this.logger.log(s.LogLevel.Debug,"Received a new batch "+e+" but errored out on a previous batch "+(this.nextBatchId-1)),[4,n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())]):[3,4]:[3,5];case 3:return o.sent(),[2];case 4:return this.logger.log(s.LogLevel.Debug,"Waiting for batch "+this.nextBatchId+". Batch "+e+" not processed."),[2];case 5:return o.trys.push([5,7,,8]),this.nextBatchId++,this.logger.log(s.LogLevel.Debug,"Applying batch "+e+"."),i.renderBatch(this.browserRendererId,new a.OutOfProcessRenderBatch(t)),[4,this.completeBatch(n,e)];case 6:return o.sent(),[3,8];case 7:throw r=o.sent(),this.fatalError=r.toString(),this.logger.log(s.LogLevel.Error,"There was an error applying batch "+e+"."),n.send("OnRenderCompleted",e,r.toString()),r;case 8:return[2]}})})},e.prototype.getLastBatchid=function(){return this.nextBatchId-1},e.prototype.completeBatch=function(e,t){return r(this,void 0,void 0,function(){return o(this,function(n){switch(n.label){case 0:return n.trys.push([0,2,,3]),[4,e.send("OnRenderCompleted",t,null)];case 1:return n.sent(),[3,3];case 2:return n.sent(),this.logger.log(s.LogLevel.Warning,"Failed to deliver completion notification for render '"+t+"'."),[3,3];case 3:return[2]}})})},e}();t.RenderQueue=c},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(72),o=Math.pow(2,32),i=Math.pow(2,21)-1,a=function(){function e(e){this.batchData=e;var t=new l(e);this.arrayRangeReader=new f(e),this.arrayBuilderSegmentReader=new h(e),this.diffReader=new s(e),this.editReader=new c(e,t),this.frameReader=new u(e,t)}return e.prototype.updatedComponents=function(){return p(this.batchData,this.batchData.length-20)},e.prototype.referenceFrames=function(){return p(this.batchData,this.batchData.length-16)},e.prototype.disposedComponentIds=function(){return p(this.batchData,this.batchData.length-12)},e.prototype.disposedEventHandlerIds=function(){return p(this.batchData,this.batchData.length-8)},e.prototype.updatedComponentsEntry=function(e,t){var n=e+4*t;return p(this.batchData,n)},e.prototype.referenceFramesEntry=function(e,t){return e+20*t},e.prototype.disposedComponentIdsEntry=function(e,t){var n=e+4*t;return p(this.batchData,n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=e+8*t;return g(this.batchData,n)},e}();t.OutOfProcessRenderBatch=a;var s=function(){function e(e){this.batchDataUint8=e}return e.prototype.componentId=function(e){return p(this.batchDataUint8,e)},e.prototype.edits=function(e){return e+4},e.prototype.editsEntry=function(e,t){return e+16*t},e}(),c=function(){function e(e,t){this.batchDataUint8=e,this.stringReader=t}return e.prototype.editType=function(e){return p(this.batchDataUint8,e)},e.prototype.siblingIndex=function(e){return p(this.batchDataUint8,e+4)},e.prototype.newTreeIndex=function(e){return p(this.batchDataUint8,e+8)},e.prototype.moveToSiblingIndex=function(e){return p(this.batchDataUint8,e+8)},e.prototype.removedAttributeName=function(e){var t=p(this.batchDataUint8,e+12);return this.stringReader.readString(t)},e}(),u=function(){function e(e,t){this.batchDataUint8=e,this.stringReader=t}return e.prototype.frameType=function(e){return p(this.batchDataUint8,e)},e.prototype.subtreeLength=function(e){return p(this.batchDataUint8,e+4)},e.prototype.elementReferenceCaptureId=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.componentId=function(e){return p(this.batchDataUint8,e+8)},e.prototype.elementName=function(e){var t=p(this.batchDataUint8,e+8);return this.stringReader.readString(t)},e.prototype.textContent=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.markupContent=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.attributeName=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.attributeValue=function(e){var t=p(this.batchDataUint8,e+8);return this.stringReader.readString(t)},e.prototype.attributeEventHandlerId=function(e){return g(this.batchDataUint8,e+12)},e}(),l=function(){function e(e){this.batchDataUint8=e,this.stringTableStartIndex=p(e,e.length-4)}return e.prototype.readString=function(e){if(-1===e)return null;var t,n=p(this.batchDataUint8,this.stringTableStartIndex+4*e),o=function(e,t){for(var n=0,r=0,o=0;o<4;o++){var i=e[t+o];if(n|=(127&i)<<r,i<128)break;r+=7}return n}(this.batchDataUint8,n),i=n+((t=o)<128?1:t<16384?2:t<2097152?3:4),a=new Uint8Array(this.batchDataUint8.buffer,this.batchDataUint8.byteOffset+i,o);return r.decodeUtf8(a)},e}(),f=function(){function e(e){this.batchDataUint8=e}return e.prototype.count=function(e){return p(this.batchDataUint8,e)},e.prototype.values=function(e){return e+4},e}(),h=function(){function e(e){this.batchDataUint8=e}return e.prototype.offset=function(e){return 0},e.prototype.count=function(e){return p(this.batchDataUint8,e)},e.prototype.values=function(e){return e+4},e}();function p(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function d(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function g(e,t){var n=d(e,t+4);if(n>i)throw new Error("Cannot read uint64 with high order part "+n+", because the result would exceed Number.MAX_SAFE_INTEGER.");return n*o+d(e,t)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r="function"==typeof TextDecoder?new TextDecoder("utf-8"):null;t.decodeUtf8=r?r.decode.bind(r):function(e){var t=0,n=e.length,r=[],o=[];for(;t<n;){var i=e[t++];if(0===i)break;if(0==(128&i))r.push(i);else if(192==(224&i)){var a=63&e[t++];r.push((31&i)<<6|a)}else if(224==(240&i)){var a=63&e[t++],s=63&e[t++];r.push((31&i)<<12|a<<6|s)}else if(240==(248&i)){var a=63&e[t++],s=63&e[t++],c=63&e[t++],u=(7&i)<<18|a<<12|s<<6|c;u>65535&&(u-=65536,r.push(u>>>10&1023|55296),u=56320|1023&u),r.push(u)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(17),o=function(){function e(){}return e.prototype.log=function(e,t){},e.instance=new e,e}();t.NullLogger=o;var i=function(){function e(e){this.minimumLogLevel=e}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.LogLevel.Critical:case r.LogLevel.Error:console.error("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;case r.LogLevel.Warning:console.warn("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;case r.LogLevel.Information:console.info("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;default:console.log("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t)}},e}();t.ConsoleLogger=i},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var i=n(13),a=n(14),s=function(){function e(e){this.circuitId=void 0,this.components=e}return e.prototype.reconnect=function(e){if(!this.circuitId)throw new Error("Circuit host not initialized.");return e.invoke("ConnectCircuit",this.circuitId)},e.prototype.initialize=function(e){if(this.circuitId)throw new Error("Circuit host '"+this.circuitId+"' already initialized.");this.circuitId=e},e.prototype.startCircuit=function(e){return r(this,void 0,void 0,function(){var t;return o(this,function(n){switch(n.label){case 0:return[4,e.invoke("StartCircuit",i.internalFunctions.getBaseURI(),i.internalFunctions.getLocationHref(),JSON.stringify(this.components.map(function(e){return e.toRecord()})))];case 1:return(t=n.sent())?(this.initialize(t),[2,!0]):[2,!1]}})})},e.prototype.resolveElement=function(e){var t=Number.parseInt(e);if(Number.isNaN(t))throw new Error("Invalid sequence number '"+e+"'.");return a.toLogicalRootCommentElement(this.components[t].start,this.components[t].end)},e}();t.CircuitDescriptor=s;var c=function(){function e(e,t,n,r,o){this.type=e,this.start=t,this.end=n,this.sequence=r,this.descriptor=o}return e.prototype.toRecord=function(){return{type:this.type,sequence:this.sequence,descriptor:this.descriptor}},e}();t.ComponentDescriptor=c,t.discoverComponents=function(e){for(var t=function e(t){if(!t.hasChildNodes())return[];for(var n=[],r=new h(t.childNodes);r.next()&&r.currentElement;){var o=l(r);if(o)n.push(o);else for(var i=e(r.currentElement),a=0;a<i.length;a++){var s=i[a];n.push(s)}}return n}(e),n=[],r=0;r<t.length;r++){var o=t[r],i=new c(o.type,o.start,o.end,o.sequence,o.descriptor);n.push(i)}return n};var u=/\W*Blazor:[^{]*(.*)$/;function l(e){var t=e.currentElement;if(t&&t.nodeType===Node.COMMENT_NODE&&t.textContent){var n=new RegExp(u).exec(t.textContent),r=n&&n[1];if(!r)return;try{return function(e,t,n){var r=JSON.parse(e),o=r.type,i=r.sequence,a=r.descriptor,s=r.prerenderId;if("server"!==o)throw new Error("Invalid component type '"+o+"'.");if(!a)throw new Error("descriptor must be defined when using a descriptor.");if(void 0===i)throw new Error("sequence must be defined when using a descriptor.");if(!Number.isInteger(i))throw new Error("Error parsing the sequence '"+i+"' for component '"+e+"'");if(s){var c=function(e,t){for(;t.next()&&t.currentElement;){var n=t.currentElement;if(n.nodeType===Node.COMMENT_NODE&&n.textContent){var r=new RegExp(u).exec(n.textContent),o=r&&r[1];if(o)return f(o,e),n}}return}(s,n);if(!c)throw new Error("Could not find an end component comment for '"+t+"'");return{type:o,sequence:i,descriptor:a,start:t,prerenderId:s,end:c}}return{type:o,sequence:i,descriptor:a,start:t}}(r,t,e)}catch(e){throw new Error("Found malformed component comment at "+t.textContent)}}}function f(e,t){var n=JSON.parse(e);if(1!==Object.keys(n).length)throw new Error("Invalid end of component comment: '"+e+"'");var r=n.prerenderId;if(!r)throw new Error("End of component comment must have a value for the prerendered property: '"+e+"'");if(r!==t)throw new Error("End of component comment prerendered property must match the start comment prerender id: '"+t+"', '"+r+"'")}var h=function(){function e(e){this.childNodes=e,this.currentIndex=-1,this.length=e.length}return e.prototype.next=function(){return this.currentIndex++,this.currentIndex<this.length?(this.currentElement=this.childNodes[this.currentIndex],!0):(this.currentElement=void 0,!1)},e}()},function(e,t,n){"use strict";var r=this&&this.__assign||function(){return(r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e}).apply(this,arguments)};Object.defineProperty(t,"__esModule",{value:!0});var o=n(17);t.resolveOptions=function(e){var t=r({},i,e);return e&&e.reconnectionOptions&&(t.reconnectionOptions=r({},i.reconnectionOptions,e.reconnectionOptions)),t};var i={configureSignalR:function(e){},logLevel:o.LogLevel.Warning,reconnectionOptions:{maxRetries:5,retryIntervalMilliseconds:3e3,dialogId:"components-reconnect-modal"}}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var i=n(77),a=n(78),s=n(17),c=function(){function e(e,t,n){this._currentReconnectionProcess=null,this._logger=e,this._reconnectionDisplay=t,this._reconnectCallback=n||function(){return window.Blazor.reconnect()}}return e.prototype.onConnectionDown=function(e,t){if(!this._reconnectionDisplay){var n=document.getElementById(e.dialogId);this._reconnectionDisplay=n?new a.UserSpecifiedDisplay(n):new i.DefaultReconnectDisplay(e.dialogId,document,this._logger)}this._currentReconnectionProcess||(this._currentReconnectionProcess=new u(e,this._logger,this._reconnectCallback,this._reconnectionDisplay))},e.prototype.onConnectionUp=function(){this._currentReconnectionProcess&&(this._currentReconnectionProcess.dispose(),this._currentReconnectionProcess=null)},e}();t.DefaultReconnectionHandler=c;var u=function(){function e(e,t,n,r){this.logger=t,this.reconnectCallback=n,this.isDisposed=!1,this.reconnectDisplay=r,this.reconnectDisplay.show(),this.attemptPeriodicReconnection(e)}return e.prototype.dispose=function(){this.isDisposed=!0,this.reconnectDisplay.hide()},e.prototype.attemptPeriodicReconnection=function(e){return r(this,void 0,void 0,function(){var t,n;return o(this,function(r){switch(r.label){case 0:t=0,r.label=1;case 1:return t<e.maxRetries?[4,this.delay(e.retryIntervalMilliseconds)]:[3,7];case 2:if(r.sent(),this.isDisposed)return[3,7];r.label=3;case 3:return r.trys.push([3,5,,6]),[4,this.reconnectCallback()];case 4:return r.sent()?[2]:(this.reconnectDisplay.rejected(),[2]);case 5:return n=r.sent(),this.logger.log(s.LogLevel.Error,n),[3,6];case 6:return t++,[3,1];case 7:return this.reconnectDisplay.failed(),[2]}})})},e.prototype.delay=function(e){return new Promise(function(t){return setTimeout(t,e)})},e}()},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var i=n(17),a=function(){function e(e,t,n){var a=this;this.document=t,this.logger=n,this.addedToDom=!1,this.modal=this.document.createElement("div"),this.modal.id=e;this.modal.style.cssText=["position: fixed","top: 0","right: 0","bottom: 0","left: 0","z-index: 1000","display: none","overflow: hidden","background-color: #fff","opacity: 0.8","text-align: center","font-weight: bold"].join(";"),this.modal.innerHTML='<h5 style="margin-top: 20px"></h5><button style="margin:5px auto 5px">Retry</button><p>Alternatively, <a href>reload</a></p>',this.message=this.modal.querySelector("h5"),this.button=this.modal.querySelector("button"),this.reloadParagraph=this.modal.querySelector("p"),this.button.addEventListener("click",function(){return r(a,void 0,void 0,function(){var e;return o(this,function(t){switch(t.label){case 0:this.show(),t.label=1;case 1:return t.trys.push([1,3,,4]),[4,window.Blazor.reconnect()];case 2:return t.sent()||this.rejected(),[3,4];case 3:return e=t.sent(),this.logger.log(i.LogLevel.Error,e),this.failed(),[3,4];case 4:return[2]}})})}),this.reloadParagraph.querySelector("a").addEventListener("click",function(){return location.reload()})}return e.prototype.show=function(){this.addedToDom||(this.addedToDom=!0,this.document.body.appendChild(this.modal)),this.modal.style.display="block",this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.textContent="Attempting to reconnect to the server..."},e.prototype.hide=function(){this.modal.style.display="none"},e.prototype.failed=function(){this.button.style.display="block",this.reloadParagraph.style.display="none",this.message.innerHTML="Reconnection failed. Try <a href>reloading</a> the page if you're unable to reconnect.",this.message.querySelector("a").addEventListener("click",function(){return location.reload()})},e.prototype.rejected=function(){this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.innerHTML="Could not reconnect to the server. <a href>Reload</a> the page to restore functionality.",this.message.querySelector("a").addEventListener("click",function(){return location.reload()})},e}();t.DefaultReconnectDisplay=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.dialog=e}return e.prototype.show=function(){this.removeClasses(),this.dialog.classList.add(e.ShowClassName)},e.prototype.hide=function(){this.removeClasses(),this.dialog.classList.add(e.HideClassName)},e.prototype.failed=function(){this.removeClasses(),this.dialog.classList.add(e.FailedClassName)},e.prototype.rejected=function(){this.removeClasses(),this.dialog.classList.add(e.RejectedClassName)},e.prototype.removeClasses=function(){this.dialog.classList.remove(e.ShowClassName,e.HideClassName,e.FailedClassName,e.RejectedClassName)},e.ShowClassName="components-reconnect-show",e.HideClassName="components-reconnect-hide",e.FailedClassName="components-reconnect-failed",e.RejectedClassName="components-reconnect-rejected",e}();t.UserSpecifiedDisplay=r},function(e,t,n){"use strict";n.r(t);var r=n(6),o=n(12),i=n(2),a=function(){function e(){}return e.write=function(e){var t=e.byteLength||e.length,n=[];do{var r=127&t;(t>>=7)>0&&(r|=128),n.push(r)}while(t>0);t=e.byteLength||e.length;var o=new Uint8Array(n.length+t);return o.set(n,0),o.set(e,n.length),o.buffer},e.parse=function(e){for(var t=[],n=new Uint8Array(e),r=[0,7,14,21,28],o=0;o<e.byteLength;){var i=0,a=0,s=void 0;do{a|=(127&(s=n[o+i]))<<r[i],i++}while(i<Math.min(5,e.byteLength-o)&&0!=(128&s));if(0!=(128&s)&&i<5)throw new Error("Cannot read message size.");if(5===i&&s>7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+i+a))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+i,o+i+a):n.subarray(o+i,o+i+a)),o=o+i+a}return t},e}();var s=new Uint8Array([145,i.MessageType.Ping]),c=function(){function e(){this.name="messagepack",this.version=1,this.transferFormat=i.TransferFormat.Binary,this.errorResult=1,this.voidResult=2,this.nonVoidResult=3}return e.prototype.parseMessages=function(e,t){if(!(e instanceof r.Buffer||(n=e,n&&"undefined"!=typeof ArrayBuffer&&(n instanceof ArrayBuffer||n.constructor&&"ArrayBuffer"===n.constructor.name))))throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer or Buffer.");var n;null===t&&(t=i.NullLogger.instance);for(var o=[],s=0,c=a.parse(e);s<c.length;s++){var u=c[s],l=this.parseMessage(u,t);l&&o.push(l)}return o},e.prototype.writeMessage=function(e){switch(e.type){case i.MessageType.Invocation:return this.writeInvocation(e);case i.MessageType.StreamInvocation:return this.writeStreamInvocation(e);case i.MessageType.StreamItem:return this.writeStreamItem(e);case i.MessageType.Completion:return this.writeCompletion(e);case i.MessageType.Ping:return a.write(s);case i.MessageType.CancelInvocation:return this.writeCancelInvocation(e);default:throw new Error("Invalid message type.")}},e.prototype.parseMessage=function(e,t){if(0===e.length)throw new Error("Invalid payload.");var n=o().decode(r.Buffer.from(e));if(0===n.length||!(n instanceof Array))throw new Error("Invalid payload.");var a=n[0];switch(a){case i.MessageType.Invocation:return this.createInvocationMessage(this.readHeaders(n),n);case i.MessageType.StreamItem:return this.createStreamItemMessage(this.readHeaders(n),n);case i.MessageType.Completion:return this.createCompletionMessage(this.readHeaders(n),n);case i.MessageType.Ping:return this.createPingMessage(n);case i.MessageType.Close:return this.createCloseMessage(n);default:return t.log(i.LogLevel.Information,"Unknown message type '"+a+"' ignored."),null}},e.prototype.createCloseMessage=function(e){if(e.length<2)throw new Error("Invalid payload for Close message.");return{error:e[1],type:i.MessageType.Close}},e.prototype.createPingMessage=function(e){if(e.length<1)throw new Error("Invalid payload for Ping message.");return{type:i.MessageType.Ping}},e.prototype.createInvocationMessage=function(e,t){if(t.length<5)throw new Error("Invalid payload for Invocation message.");var n=t[2];return n?{arguments:t[4],headers:e,invocationId:n,streamIds:[],target:t[3],type:i.MessageType.Invocation}:{arguments:t[4],headers:e,streamIds:[],target:t[3],type:i.MessageType.Invocation}},e.prototype.createStreamItemMessage=function(e,t){if(t.length<4)throw new Error("Invalid payload for StreamItem message.");return{headers:e,invocationId:t[2],item:t[3],type:i.MessageType.StreamItem}},e.prototype.createCompletionMessage=function(e,t){if(t.length<4)throw new Error("Invalid payload for Completion message.");var n,r,o=t[3];if(o!==this.voidResult&&t.length<5)throw new Error("Invalid payload for Completion message.");switch(o){case this.errorResult:n=t[4];break;case this.nonVoidResult:r=t[4]}return{error:n,headers:e,invocationId:t[2],result:r,type:i.MessageType.Completion}},e.prototype.writeInvocation=function(e){var t=o().encode([i.MessageType.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments,e.streamIds]);return a.write(t.slice())},e.prototype.writeStreamInvocation=function(e){var t=o().encode([i.MessageType.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments,e.streamIds]);return a.write(t.slice())},e.prototype.writeStreamItem=function(e){var t=o().encode([i.MessageType.StreamItem,e.headers||{},e.invocationId,e.item]);return a.write(t.slice())},e.prototype.writeCompletion=function(e){var t,n=o(),r=e.error?this.errorResult:e.result?this.nonVoidResult:this.voidResult;switch(r){case this.errorResult:t=n.encode([i.MessageType.Completion,e.headers||{},e.invocationId,r,e.error]);break;case this.voidResult:t=n.encode([i.MessageType.Completion,e.headers||{},e.invocationId,r]);break;case this.nonVoidResult:t=n.encode([i.MessageType.Completion,e.headers||{},e.invocationId,r,e.result])}return a.write(t.slice())},e.prototype.writeCancelInvocation=function(e){var t=o().encode([i.MessageType.CancelInvocation,e.headers||{},e.invocationId]);return a.write(t.slice())},e.prototype.readHeaders=function(e){var t=e[1];if("object"!=typeof t)throw new Error("Invalid headers.");return t},e}();n.d(t,"VERSION",function(){return u}),n.d(t,"MessagePackHubProtocol",function(){return c});var u="3.1.0-dev"}]);
\ No newline at end of file +function r(e,t){if(e===t)return 0;for(var n=e.length,r=t.length,o=0,i=Math.min(n,r);o<i;++o)if(e[o]!==t[o]){n=e[o],r=t[o];break}return n<r?-1:r<n?1:0}function o(e){return t.Buffer&&"function"==typeof t.Buffer.isBuffer?t.Buffer.isBuffer(e):!(null==e||!e._isBuffer)}var i=n(35),a=Object.prototype.hasOwnProperty,s=Array.prototype.slice,c="foo"===function(){}.name;function u(e){return Object.prototype.toString.call(e)}function l(e){return!o(e)&&("function"==typeof t.ArrayBuffer&&("function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):!!e&&(e instanceof DataView||!!(e.buffer&&e.buffer instanceof ArrayBuffer))))}var f=e.exports=v,h=/\s*function\s+([^\(\s]*)\s*/;function p(e){if(i.isFunction(e)){if(c)return e.name;var t=e.toString().match(h);return t&&t[1]}}function d(e,t){return"string"==typeof e?e.length<t?e:e.slice(0,t):e}function g(e){if(c||!i.isFunction(e))return i.inspect(e);var t=p(e);return"[Function"+(t?": "+t:"")+"]"}function y(e,t,n,r,o){throw new f.AssertionError({message:n,actual:e,expected:t,operator:r,stackStartFunction:o})}function v(e,t){e||y(e,!0,t,"==",f.ok)}function b(e,t,n,a){if(e===t)return!0;if(o(e)&&o(t))return 0===r(e,t);if(i.isDate(e)&&i.isDate(t))return e.getTime()===t.getTime();if(i.isRegExp(e)&&i.isRegExp(t))return e.source===t.source&&e.global===t.global&&e.multiline===t.multiline&&e.lastIndex===t.lastIndex&&e.ignoreCase===t.ignoreCase;if(null!==e&&"object"==typeof e||null!==t&&"object"==typeof t){if(l(e)&&l(t)&&u(e)===u(t)&&!(e instanceof Float32Array||e instanceof Float64Array))return 0===r(new Uint8Array(e.buffer),new Uint8Array(t.buffer));if(o(e)!==o(t))return!1;var c=(a=a||{actual:[],expected:[]}).actual.indexOf(e);return-1!==c&&c===a.expected.indexOf(t)||(a.actual.push(e),a.expected.push(t),function(e,t,n,r){if(null==e||null==t)return!1;if(i.isPrimitive(e)||i.isPrimitive(t))return e===t;if(n&&Object.getPrototypeOf(e)!==Object.getPrototypeOf(t))return!1;var o=m(e),a=m(t);if(o&&!a||!o&&a)return!1;if(o)return e=s.call(e),t=s.call(t),b(e,t,n);var c,u,l=S(e),f=S(t);if(l.length!==f.length)return!1;for(l.sort(),f.sort(),u=l.length-1;u>=0;u--)if(l[u]!==f[u])return!1;for(u=l.length-1;u>=0;u--)if(c=l[u],!b(e[c],t[c],n,r))return!1;return!0}(e,t,n,a))}return n?e===t:e==t}function m(e){return"[object Arguments]"==Object.prototype.toString.call(e)}function w(e,t){if(!e||!t)return!1;if("[object RegExp]"==Object.prototype.toString.call(t))return t.test(e);try{if(e instanceof t)return!0}catch(e){}return!Error.isPrototypeOf(t)&&!0===t.call({},e)}function E(e,t,n,r){var o;if("function"!=typeof t)throw new TypeError('"block" argument must be a function');"string"==typeof n&&(r=n,n=null),o=function(e){var t;try{e()}catch(e){t=e}return t}(t),r=(n&&n.name?" ("+n.name+").":".")+(r?" "+r:"."),e&&!o&&y(o,n,"Missing expected exception"+r);var a="string"==typeof r,s=!e&&o&&!n;if((!e&&i.isError(o)&&a&&w(o,n)||s)&&y(o,n,"Got unwanted exception"+r),e&&o&&n&&!w(o,n)||!e&&o)throw o}f.AssertionError=function(e){var t;this.name="AssertionError",this.actual=e.actual,this.expected=e.expected,this.operator=e.operator,e.message?(this.message=e.message,this.generatedMessage=!1):(this.message=d(g((t=this).actual),128)+" "+t.operator+" "+d(g(t.expected),128),this.generatedMessage=!0);var n=e.stackStartFunction||y;if(Error.captureStackTrace)Error.captureStackTrace(this,n);else{var r=new Error;if(r.stack){var o=r.stack,i=p(n),a=o.indexOf("\n"+i);if(a>=0){var s=o.indexOf("\n",a+1);o=o.substring(s+1)}this.stack=o}}},i.inherits(f.AssertionError,Error),f.fail=y,f.ok=v,f.equal=function(e,t,n){e!=t&&y(e,t,n,"==",f.equal)},f.notEqual=function(e,t,n){e==t&&y(e,t,n,"!=",f.notEqual)},f.deepEqual=function(e,t,n){b(e,t,!1)||y(e,t,n,"deepEqual",f.deepEqual)},f.deepStrictEqual=function(e,t,n){b(e,t,!0)||y(e,t,n,"deepStrictEqual",f.deepStrictEqual)},f.notDeepEqual=function(e,t,n){b(e,t,!1)&&y(e,t,n,"notDeepEqual",f.notDeepEqual)},f.notDeepStrictEqual=function e(t,n,r){b(t,n,!0)&&y(t,n,r,"notDeepStrictEqual",e)},f.strictEqual=function(e,t,n){e!==t&&y(e,t,n,"===",f.strictEqual)},f.notStrictEqual=function(e,t,n){e===t&&y(e,t,n,"!==",f.notStrictEqual)},f.throws=function(e,t,n){E(!0,e,t,n)},f.doesNotThrow=function(e,t,n){E(!1,e,t,n)},f.ifError=function(e){if(e)throw e};var S=Object.keys||function(e){var t=[];for(var n in e)a.call(e,n)&&t.push(n);return t}}).call(this,n(10))},function(e,t){e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){e.exports=n(11)},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t){},function(e,t,n){"use strict";var r=n(15).Buffer,o=n(60);e.exports=function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.head=null,this.tail=null,this.length=0}return e.prototype.push=function(e){var t={data:e,next:null};this.length>0?this.tail.next=t:this.head=t,this.tail=t,++this.length},e.prototype.unshift=function(e){var t={data:e,next:this.head};0===this.length&&(this.tail=t),this.head=t,++this.length},e.prototype.shift=function(){if(0!==this.length){var e=this.head.data;return 1===this.length?this.head=this.tail=null:this.head=this.head.next,--this.length,e}},e.prototype.clear=function(){this.head=this.tail=null,this.length=0},e.prototype.join=function(e){if(0===this.length)return"";for(var t=this.head,n=""+t.data;t=t.next;)n+=e+t.data;return n},e.prototype.concat=function(e){if(0===this.length)return r.alloc(0);if(1===this.length)return this.head.data;for(var t,n,o,i=r.allocUnsafe(e>>>0),a=this.head,s=0;a;)t=a.data,n=i,o=s,t.copy(n,o),s+=a.data.length,a=a.next;return i},e}(),o&&o.inspect&&o.inspect.custom&&(e.exports.prototype[o.inspect.custom]=function(){var e=o.inspect({length:this.length});return this.constructor.name+" "+e})},function(e,t){},function(e,t,n){var r=n(6),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=a),i(o,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){(function(e){var r=void 0!==e&&e||"undefined"!=typeof self&&self||window,o=Function.prototype.apply;function i(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new i(o.call(setTimeout,r,arguments),clearTimeout)},t.setInterval=function(){return new i(o.call(setInterval,r,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(r,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(63),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(10))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var r,o,i,a,s,c=1,u={},l=!1,f=e.document,h=Object.getPrototypeOf&&Object.getPrototypeOf(e);h=h&&h.setTimeout?h:e,"[object process]"==={}.toString.call(e.process)?r=function(e){t.nextTick(function(){d(e)})}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((i=new MessageChannel).port1.onmessage=function(e){d(e.data)},r=function(e){i.port2.postMessage(e)}):f&&"onreadystatechange"in f.createElement("script")?(o=f.documentElement,r=function(e){var t=f.createElement("script");t.onreadystatechange=function(){d(e),t.onreadystatechange=null,o.removeChild(t),t=null},o.appendChild(t)}):r=function(e){setTimeout(d,0,e)}:(a="setImmediate$"+Math.random()+"$",s=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(a)&&d(+t.data.slice(a.length))},e.addEventListener?e.addEventListener("message",s,!1):e.attachEvent("onmessage",s),r=function(t){e.postMessage(a+t,"*")}),h.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n<t.length;n++)t[n]=arguments[n+1];var o={callback:e,args:t};return u[c]=o,r(c),c++},h.clearImmediate=p}function p(e){delete u[e]}function d(e){if(l)setTimeout(d,0,e);else{var t=u[e];if(t){l=!0;try{!function(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}(t)}finally{p(e),l=!1}}}}}("undefined"==typeof self?void 0===e?this:e:self)}).call(this,n(10),n(20))},function(e,t,n){(function(t){function n(e){try{if(!t.localStorage)return!1}catch(e){return!1}var n=t.localStorage[e];return null!=n&&"true"===String(n).toLowerCase()}e.exports=function(e,t){if(n("noDeprecation"))return e;var r=!1;return function(){if(!r){if(n("throwDeprecation"))throw new Error(t);n("traceDeprecation")?console.trace(t):console.warn(t),r=!0}return e.apply(this,arguments)}}}).call(this,n(10))},function(e,t,n){"use strict";var r=n(66).Transform,o=n(16),i=n(22);function a(e){(e=e||{}).objectMode=!0,e.highWaterMark=16,r.call(this,e),this._msgpack=e.msgpack}function s(e){if(!(this instanceof s))return(e=e||{}).msgpack=this,new s(e);a.call(this,e)}function c(e){if(!(this instanceof c))return(e=e||{}).msgpack=this,new c(e);a.call(this,e),this._chunks=i()}o(a,r),o(s,a),s.prototype._transform=function(e,t,n){var r=null;try{r=this._msgpack.encode(e).slice(0)}catch(e){return this.emit("error",e),n()}this.push(r),n()},o(c,a),c.prototype._transform=function(e,t,n){e&&this._chunks.append(e);try{var r=this._msgpack.decode(this._chunks);this.push(r)}catch(e){return void(e instanceof this._msgpack.IncompleteBufferError?n():this.emit("error",e))}this._chunks.length>0?this._transform(null,t,n):n()},e.exports.decoder=c,e.exports.encoder=s},function(e,t,n){(t=e.exports=n(36)).Stream=t,t.Readable=t,t.Writable=n(41),t.Duplex=n(11),t.Transform=n(42),t.PassThrough=n(67)},function(e,t,n){"use strict";e.exports=i;var r=n(42),o=n(21);function i(e){if(!(this instanceof i))return new i(e);r.call(this,e)}o.inherits=n(16),o.inherits(i,r),i.prototype._transform=function(e,t,n){n(null,e)}},function(e,t,n){var r=n(22);function o(e){Error.call(this),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.name=this.constructor.name,this.message=e||"unable to decode"}n(35).inherits(o,Error),e.exports=function(e){return function(e){e instanceof r||(e=r().append(e));var t=i(e);if(t)return e.consume(t.bytesConsumed),t.value;throw new o};function t(e,t,n){return t>=n+e}function n(e,t){return{value:e,bytesConsumed:t}}function i(e,r){r=void 0===r?0:r;var o=e.length-r;if(o<=0)return null;var i,l,f,h=e.readUInt8(r),p=0;if(!function(e,t){var n=function(e){switch(e){case 196:return 2;case 197:return 3;case 198:return 5;case 199:return 3;case 200:return 4;case 201:return 6;case 202:return 5;case 203:return 9;case 204:return 2;case 205:return 3;case 206:return 5;case 207:return 9;case 208:return 2;case 209:return 3;case 210:return 5;case 211:return 9;case 212:return 3;case 213:return 4;case 214:return 6;case 215:return 10;case 216:return 18;case 217:return 2;case 218:return 3;case 219:return 5;case 222:return 3;default:return-1}}(e);return!(-1!==n&&t<n)}(h,o))return null;switch(h){case 192:return n(null,1);case 194:return n(!1,1);case 195:return n(!0,1);case 204:return n(p=e.readUInt8(r+1),2);case 205:return n(p=e.readUInt16BE(r+1),3);case 206:return n(p=e.readUInt32BE(r+1),5);case 207:for(f=7;f>=0;f--)p+=e.readUInt8(r+f+1)*Math.pow(2,8*(7-f));return n(p,9);case 208:return n(p=e.readInt8(r+1),2);case 209:return n(p=e.readInt16BE(r+1),3);case 210:return n(p=e.readInt32BE(r+1),5);case 211:return n(p=function(e,t){var n=128==(128&e[t]);if(n)for(var r=1,o=t+7;o>=t;o--){var i=(255^e[o])+r;e[o]=255&i,r=i>>8}var a=e.readUInt32BE(t+0),s=e.readUInt32BE(t+4);return(4294967296*a+s)*(n?-1:1)}(e.slice(r+1,r+9),0),9);case 202:return n(p=e.readFloatBE(r+1),5);case 203:return n(p=e.readDoubleBE(r+1),9);case 217:return t(i=e.readUInt8(r+1),o,2)?n(p=e.toString("utf8",r+2,r+2+i),2+i):null;case 218:return t(i=e.readUInt16BE(r+1),o,3)?n(p=e.toString("utf8",r+3,r+3+i),3+i):null;case 219:return t(i=e.readUInt32BE(r+1),o,5)?n(p=e.toString("utf8",r+5,r+5+i),5+i):null;case 196:return t(i=e.readUInt8(r+1),o,2)?n(p=e.slice(r+2,r+2+i),2+i):null;case 197:return t(i=e.readUInt16BE(r+1),o,3)?n(p=e.slice(r+3,r+3+i),3+i):null;case 198:return t(i=e.readUInt32BE(r+1),o,5)?n(p=e.slice(r+5,r+5+i),5+i):null;case 220:return o<3?null:(i=e.readUInt16BE(r+1),a(e,r,i,3));case 221:return o<5?null:(i=e.readUInt32BE(r+1),a(e,r,i,5));case 222:return i=e.readUInt16BE(r+1),s(e,r,i,3);case 223:throw new Error("map too big to decode in JS");case 212:return c(e,r,1);case 213:return c(e,r,2);case 214:return c(e,r,4);case 215:return c(e,r,8);case 216:return c(e,r,16);case 199:return i=e.readUInt8(r+1),l=e.readUInt8(r+2),t(i,o,3)?u(e,r,l,i,3):null;case 200:return i=e.readUInt16BE(r+1),l=e.readUInt8(r+3),t(i,o,4)?u(e,r,l,i,4):null;case 201:return i=e.readUInt32BE(r+1),l=e.readUInt8(r+5),t(i,o,6)?u(e,r,l,i,6):null}if(144==(240&h))return a(e,r,i=15&h,1);if(128==(240&h))return s(e,r,i=15&h,1);if(160==(224&h))return t(i=31&h,o,1)?n(p=e.toString("utf8",r+1,r+i+1),i+1):null;if(h>=224)return n(p=h-256,1);if(h<128)return n(h,1);throw new Error("not implemented yet")}function a(e,t,r,o){var a,s=[],c=0;for(t+=o,a=0;a<r;a++){var u=i(e,t);if(!u)return null;s.push(u.value),t+=u.bytesConsumed,c+=u.bytesConsumed}return n(s,o+c)}function s(e,t,r,o){var a,s={},c=0;for(t+=o,a=0;a<r;a++){var u=i(e,t);if(!u)return null;var l=i(e,t+=u.bytesConsumed);if(!l)return null;s[u.value]=l.value,t+=l.bytesConsumed,c+=u.bytesConsumed+l.bytesConsumed}return n(s,o+c)}function c(e,t,n){var r=e.readInt8(t+1);return u(e,t,r,n,2)}function u(t,r,o,i,a){var s,c;if(r+=a,o<0)switch(o){case-1:return function(e,t,r){var o,i;switch(i=0,t){case 4:o=e.readUInt32BE(0);break;case 8:var a=e.readUInt32BE(0),s=e.readUInt32BE(4);i=a/4,o=(3&a)*Math.pow(2,32)+s;break;case 12:throw new Error("timestamp 96 is not yet implemented")}var c=1e3*o+Math.round(i/1e6);return n(new Date(c),t+r)}(c=t.slice(r,r+i),i,a)}for(s=0;s<e.length;s++){if(o===e[s].type)return c=t.slice(r,r+i),n(e[s].decode(c),a+i)}throw new Error("unable to find ext type "+o)}},e.exports.IncompleteBufferError=o},function(e,t,n){"use strict";var r=n(15).Buffer,o=n(22),i=.1;function a(e,t){var n;return(n=r.allocUnsafe(5))[0]=202,n.writeFloatBE(e,1),(t||Math.abs(e-n.readFloatBE(1))>i)&&((n=r.allocUnsafe(9))[0]=203,n.writeDoubleBE(e,1)),n}e.exports=function(e,t,n,i){function s(c,u){var l,f,h;if(void 0===c)throw new Error("undefined is not encodable in msgpack!");if(null===c)(l=r.allocUnsafe(1))[0]=192;else if(!0===c)(l=r.allocUnsafe(1))[0]=195;else if(!1===c)(l=r.allocUnsafe(1))[0]=194;else if("string"==typeof c)(f=r.byteLength(c))<32?((l=r.allocUnsafe(1+f))[0]=160|f,f>0&&l.write(c,1)):f<=255&&!n?((l=r.allocUnsafe(2+f))[0]=217,l[1]=f,l.write(c,2)):f<=65535?((l=r.allocUnsafe(3+f))[0]=218,l.writeUInt16BE(f,1),l.write(c,3)):((l=r.allocUnsafe(5+f))[0]=219,l.writeUInt32BE(f,1),l.write(c,5));else if(c&&(c.readUInt32LE||c instanceof Uint8Array))c instanceof Uint8Array&&(c=r.from(c)),c.length<=255?((l=r.allocUnsafe(2))[0]=196,l[1]=c.length):c.length<=65535?((l=r.allocUnsafe(3))[0]=197,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=198,l.writeUInt32BE(c.length,1)),l=o([l,c]);else if(Array.isArray(c))c.length<16?(l=r.allocUnsafe(1))[0]=144|c.length:c.length<65536?((l=r.allocUnsafe(3))[0]=220,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=221,l.writeUInt32BE(c.length,1)),l=c.reduce(function(e,t){return e.append(s(t,!0)),e},o().append(l));else{if(!i&&"function"==typeof c.getDate)return function(e){var t,n=1*e,i=Math.floor(n/1e3),a=1e6*(n-1e3*i);if(a||i>4294967295){(t=new r(10))[0]=215,t[1]=-1;var s=4*a,c=i/Math.pow(2,32),u=s+c&4294967295,l=4294967295&i;t.writeInt32BE(u,2),t.writeInt32BE(l,6)}else(t=new r(6))[0]=214,t[1]=-1,t.writeUInt32BE(Math.floor(n/1e3),2);return o().append(t)}(c);if("object"==typeof c)l=function(t){var n,i,a=-1,s=[];for(n=0;n<e.length;n++)if(e[n].check(t)){i=e[n].encode(t);break}if(!i)return null;1==(a=i.length-1)?s.push(212):2===a?s.push(213):4===a?s.push(214):8===a?s.push(215):16===a?s.push(216):a<256?(s.push(199),s.push(a)):a<65536?(s.push(200),s.push(a>>8),s.push(255&a)):(s.push(201),s.push(a>>24),s.push(a>>16&255),s.push(a>>8&255),s.push(255&a));return o().append(r.from(s)).append(i)}(c)||function(e){var t,n,i=[],a=0;for(t in e)e.hasOwnProperty(t)&&void 0!==e[t]&&"function"!=typeof e[t]&&(++a,i.push(s(t,!0)),i.push(s(e[t],!0)));a<16?(n=r.allocUnsafe(1))[0]=128|a:((n=r.allocUnsafe(3))[0]=222,n.writeUInt16BE(a,1));return i.unshift(n),i.reduce(function(e,t){return e.append(t)},o())}(c);else if("number"==typeof c){if((h=c)!==Math.floor(h))return a(c,t);if(c>=0)if(c<128)(l=r.allocUnsafe(1))[0]=c;else if(c<256)(l=r.allocUnsafe(2))[0]=204,l[1]=c;else if(c<65536)(l=r.allocUnsafe(3))[0]=205,l.writeUInt16BE(c,1);else if(c<=4294967295)(l=r.allocUnsafe(5))[0]=206,l.writeUInt32BE(c,1);else{if(!(c<=9007199254740991))return a(c,!0);(l=r.allocUnsafe(9))[0]=207,function(e,t){for(var n=7;n>=0;n--)e[n+1]=255&t,t/=256}(l,c)}else if(c>=-32)(l=r.allocUnsafe(1))[0]=256+c;else if(c>=-128)(l=r.allocUnsafe(2))[0]=208,l.writeInt8(c,1);else if(c>=-32768)(l=r.allocUnsafe(3))[0]=209,l.writeInt16BE(c,1);else if(c>-214748365)(l=r.allocUnsafe(5))[0]=210,l.writeInt32BE(c,1);else{if(!(c>=-9007199254740991))return a(c,!0);(l=r.allocUnsafe(9))[0]=211,function(e,t,n){var r=n<0;r&&(n=Math.abs(n));var o=n%4294967296,i=n/4294967296;if(e.writeUInt32BE(Math.floor(i),t+0),e.writeUInt32BE(o,t+4),r)for(var a=1,s=t+7;s>=t;s--){var c=(255^e[s])+a;e[s]=255&c,a=c>>8}}(l,1,c)}}}if(!l)throw new Error("not implemented yet");return u?l:l.slice()}return s}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var i=n(5),a=n(71),s=n(17),c=function(){function e(e,t){this.nextBatchId=2,this.browserRendererId=e,this.logger=t}return e.getOrCreate=function(t){return e.instance||(e.instance=new e(0,t)),this.instance},e.prototype.processBatch=function(e,t,n){return r(this,void 0,void 0,function(){var r;return o(this,function(o){switch(o.label){case 0:return e<this.nextBatchId?[4,this.completeBatch(n,e)]:[3,2];case 1:return o.sent(),this.logger.log(s.LogLevel.Debug,"Batch "+e+" already processed. Waiting for batch "+this.nextBatchId+"."),[2];case 2:return e>this.nextBatchId?this.fatalError?(this.logger.log(s.LogLevel.Debug,"Received a new batch "+e+" but errored out on a previous batch "+(this.nextBatchId-1)),[4,n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())]):[3,4]:[3,5];case 3:return o.sent(),[2];case 4:return this.logger.log(s.LogLevel.Debug,"Waiting for batch "+this.nextBatchId+". Batch "+e+" not processed."),[2];case 5:return o.trys.push([5,7,,8]),this.nextBatchId++,this.logger.log(s.LogLevel.Debug,"Applying batch "+e+"."),i.renderBatch(this.browserRendererId,new a.OutOfProcessRenderBatch(t)),[4,this.completeBatch(n,e)];case 6:return o.sent(),[3,8];case 7:throw r=o.sent(),this.fatalError=r.toString(),this.logger.log(s.LogLevel.Error,"There was an error applying batch "+e+"."),n.send("OnRenderCompleted",e,r.toString()),r;case 8:return[2]}})})},e.prototype.getLastBatchid=function(){return this.nextBatchId-1},e.prototype.completeBatch=function(e,t){return r(this,void 0,void 0,function(){return o(this,function(n){switch(n.label){case 0:return n.trys.push([0,2,,3]),[4,e.send("OnRenderCompleted",t,null)];case 1:return n.sent(),[3,3];case 2:return n.sent(),this.logger.log(s.LogLevel.Warning,"Failed to deliver completion notification for render '"+t+"'."),[3,3];case 3:return[2]}})})},e}();t.RenderQueue=c},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(72),o=Math.pow(2,32),i=Math.pow(2,21)-1,a=function(){function e(e){this.batchData=e;var t=new l(e);this.arrayRangeReader=new f(e),this.arrayBuilderSegmentReader=new h(e),this.diffReader=new s(e),this.editReader=new c(e,t),this.frameReader=new u(e,t)}return e.prototype.updatedComponents=function(){return p(this.batchData,this.batchData.length-20)},e.prototype.referenceFrames=function(){return p(this.batchData,this.batchData.length-16)},e.prototype.disposedComponentIds=function(){return p(this.batchData,this.batchData.length-12)},e.prototype.disposedEventHandlerIds=function(){return p(this.batchData,this.batchData.length-8)},e.prototype.updatedComponentsEntry=function(e,t){var n=e+4*t;return p(this.batchData,n)},e.prototype.referenceFramesEntry=function(e,t){return e+20*t},e.prototype.disposedComponentIdsEntry=function(e,t){var n=e+4*t;return p(this.batchData,n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=e+8*t;return g(this.batchData,n)},e}();t.OutOfProcessRenderBatch=a;var s=function(){function e(e){this.batchDataUint8=e}return e.prototype.componentId=function(e){return p(this.batchDataUint8,e)},e.prototype.edits=function(e){return e+4},e.prototype.editsEntry=function(e,t){return e+16*t},e}(),c=function(){function e(e,t){this.batchDataUint8=e,this.stringReader=t}return e.prototype.editType=function(e){return p(this.batchDataUint8,e)},e.prototype.siblingIndex=function(e){return p(this.batchDataUint8,e+4)},e.prototype.newTreeIndex=function(e){return p(this.batchDataUint8,e+8)},e.prototype.moveToSiblingIndex=function(e){return p(this.batchDataUint8,e+8)},e.prototype.removedAttributeName=function(e){var t=p(this.batchDataUint8,e+12);return this.stringReader.readString(t)},e}(),u=function(){function e(e,t){this.batchDataUint8=e,this.stringReader=t}return e.prototype.frameType=function(e){return p(this.batchDataUint8,e)},e.prototype.subtreeLength=function(e){return p(this.batchDataUint8,e+4)},e.prototype.elementReferenceCaptureId=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.componentId=function(e){return p(this.batchDataUint8,e+8)},e.prototype.elementName=function(e){var t=p(this.batchDataUint8,e+8);return this.stringReader.readString(t)},e.prototype.textContent=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.markupContent=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.attributeName=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.attributeValue=function(e){var t=p(this.batchDataUint8,e+8);return this.stringReader.readString(t)},e.prototype.attributeEventHandlerId=function(e){return g(this.batchDataUint8,e+12)},e}(),l=function(){function e(e){this.batchDataUint8=e,this.stringTableStartIndex=p(e,e.length-4)}return e.prototype.readString=function(e){if(-1===e)return null;var t,n=p(this.batchDataUint8,this.stringTableStartIndex+4*e),o=function(e,t){for(var n=0,r=0,o=0;o<4;o++){var i=e[t+o];if(n|=(127&i)<<r,i<128)break;r+=7}return n}(this.batchDataUint8,n),i=n+((t=o)<128?1:t<16384?2:t<2097152?3:4),a=new Uint8Array(this.batchDataUint8.buffer,this.batchDataUint8.byteOffset+i,o);return r.decodeUtf8(a)},e}(),f=function(){function e(e){this.batchDataUint8=e}return e.prototype.count=function(e){return p(this.batchDataUint8,e)},e.prototype.values=function(e){return e+4},e}(),h=function(){function e(e){this.batchDataUint8=e}return e.prototype.offset=function(e){return 0},e.prototype.count=function(e){return p(this.batchDataUint8,e)},e.prototype.values=function(e){return e+4},e}();function p(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function d(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function g(e,t){var n=d(e,t+4);if(n>i)throw new Error("Cannot read uint64 with high order part "+n+", because the result would exceed Number.MAX_SAFE_INTEGER.");return n*o+d(e,t)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r="function"==typeof TextDecoder?new TextDecoder("utf-8"):null;t.decodeUtf8=r?r.decode.bind(r):function(e){var t=0,n=e.length,r=[],o=[];for(;t<n;){var i=e[t++];if(0===i)break;if(0==(128&i))r.push(i);else if(192==(224&i)){var a=63&e[t++];r.push((31&i)<<6|a)}else if(224==(240&i)){var a=63&e[t++],s=63&e[t++];r.push((31&i)<<12|a<<6|s)}else if(240==(248&i)){var a=63&e[t++],s=63&e[t++],c=63&e[t++],u=(7&i)<<18|a<<12|s<<6|c;u>65535&&(u-=65536,r.push(u>>>10&1023|55296),u=56320|1023&u),r.push(u)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(17),o=function(){function e(){}return e.prototype.log=function(e,t){},e.instance=new e,e}();t.NullLogger=o;var i=function(){function e(e){this.minimumLogLevel=e}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.LogLevel.Critical:case r.LogLevel.Error:console.error("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;case r.LogLevel.Warning:console.warn("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;case r.LogLevel.Information:console.info("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;default:console.log("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t)}},e}();t.ConsoleLogger=i},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var i=n(13),a=n(14),s=function(){function e(e){this.circuitId=void 0,this.components=e}return e.prototype.reconnect=function(e){if(!this.circuitId)throw new Error("Circuit host not initialized.");return e.invoke("ConnectCircuit",this.circuitId)},e.prototype.initialize=function(e){if(this.circuitId)throw new Error("Circuit host '"+this.circuitId+"' already initialized.");this.circuitId=e},e.prototype.startCircuit=function(e){return r(this,void 0,void 0,function(){var t;return o(this,function(n){switch(n.label){case 0:return[4,e.invoke("StartCircuit",i.internalFunctions.getBaseURI(),i.internalFunctions.getLocationHref(),JSON.stringify(this.components.map(function(e){return e.toRecord()})))];case 1:return(t=n.sent())?(this.initialize(t),[2,!0]):[2,!1]}})})},e.prototype.resolveElement=function(e){var t=Number.parseInt(e);if(Number.isNaN(t))throw new Error("Invalid sequence number '"+e+"'.");return a.toLogicalRootCommentElement(this.components[t].start,this.components[t].end)},e}();t.CircuitDescriptor=s;var c=function(){function e(e,t,n,r,o){this.type=e,this.start=t,this.end=n,this.sequence=r,this.descriptor=o}return e.prototype.toRecord=function(){return{type:this.type,sequence:this.sequence,descriptor:this.descriptor}},e}();t.ComponentDescriptor=c,t.discoverComponents=function(e){for(var t=function e(t){if(!t.hasChildNodes())return[];for(var n=[],r=new h(t.childNodes);r.next()&&r.currentElement;){var o=l(r);if(o)n.push(o);else for(var i=e(r.currentElement),a=0;a<i.length;a++){var s=i[a];n.push(s)}}return n}(e),n=[],r=0;r<t.length;r++){var o=t[r],i=new c(o.type,o.start,o.end,o.sequence,o.descriptor);n.push(i)}return n};var u=/\W*Blazor:[^{]*(.*)$/;function l(e){var t=e.currentElement;if(t&&t.nodeType===Node.COMMENT_NODE&&t.textContent){var n=new RegExp(u).exec(t.textContent),r=n&&n[1];if(!r)return;try{return function(e,t,n){var r=JSON.parse(e),o=r.type,i=r.sequence,a=r.descriptor,s=r.prerenderId;if("server"!==o)throw new Error("Invalid component type '"+o+"'.");if(!a)throw new Error("descriptor must be defined when using a descriptor.");if(void 0===i)throw new Error("sequence must be defined when using a descriptor.");if(!Number.isInteger(i))throw new Error("Error parsing the sequence '"+i+"' for component '"+e+"'");if(s){var c=function(e,t){for(;t.next()&&t.currentElement;){var n=t.currentElement;if(n.nodeType===Node.COMMENT_NODE&&n.textContent){var r=new RegExp(u).exec(n.textContent),o=r&&r[1];if(o)return f(o,e),n}}return}(s,n);if(!c)throw new Error("Could not find an end component comment for '"+t+"'");return{type:o,sequence:i,descriptor:a,start:t,prerenderId:s,end:c}}return{type:o,sequence:i,descriptor:a,start:t}}(r,t,e)}catch(e){throw new Error("Found malformed component comment at "+t.textContent)}}}function f(e,t){var n=JSON.parse(e);if(1!==Object.keys(n).length)throw new Error("Invalid end of component comment: '"+e+"'");var r=n.prerenderId;if(!r)throw new Error("End of component comment must have a value for the prerendered property: '"+e+"'");if(r!==t)throw new Error("End of component comment prerendered property must match the start comment prerender id: '"+t+"', '"+r+"'")}var h=function(){function e(e){this.childNodes=e,this.currentIndex=-1,this.length=e.length}return e.prototype.next=function(){return this.currentIndex++,this.currentIndex<this.length?(this.currentElement=this.childNodes[this.currentIndex],!0):(this.currentElement=void 0,!1)},e}()},function(e,t,n){"use strict";var r=this&&this.__assign||function(){return(r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e}).apply(this,arguments)};Object.defineProperty(t,"__esModule",{value:!0});var o=n(17);t.resolveOptions=function(e){var t=r({},i,e);return e&&e.reconnectionOptions&&(t.reconnectionOptions=r({},i.reconnectionOptions,e.reconnectionOptions)),t};var i={configureSignalR:function(e){},logLevel:o.LogLevel.Warning,reconnectionOptions:{maxRetries:5,retryIntervalMilliseconds:3e3,dialogId:"components-reconnect-modal"}}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var i=n(77),a=n(78),s=n(17),c=function(){function e(e,t,n){this._currentReconnectionProcess=null,this._logger=e,this._reconnectionDisplay=t,this._reconnectCallback=n||function(){return window.Blazor.reconnect()}}return e.prototype.onConnectionDown=function(e,t){if(!this._reconnectionDisplay){var n=document.getElementById(e.dialogId);this._reconnectionDisplay=n?new a.UserSpecifiedDisplay(n):new i.DefaultReconnectDisplay(e.dialogId,document,this._logger)}this._currentReconnectionProcess||(this._currentReconnectionProcess=new u(e,this._logger,this._reconnectCallback,this._reconnectionDisplay))},e.prototype.onConnectionUp=function(){this._currentReconnectionProcess&&(this._currentReconnectionProcess.dispose(),this._currentReconnectionProcess=null)},e}();t.DefaultReconnectionHandler=c;var u=function(){function e(e,t,n,r){this.logger=t,this.reconnectCallback=n,this.isDisposed=!1,this.reconnectDisplay=r,this.reconnectDisplay.show(),this.attemptPeriodicReconnection(e)}return e.prototype.dispose=function(){this.isDisposed=!0,this.reconnectDisplay.hide()},e.prototype.attemptPeriodicReconnection=function(e){return r(this,void 0,void 0,function(){var t,n;return o(this,function(r){switch(r.label){case 0:t=0,r.label=1;case 1:return t<e.maxRetries?[4,this.delay(e.retryIntervalMilliseconds)]:[3,7];case 2:if(r.sent(),this.isDisposed)return[3,7];r.label=3;case 3:return r.trys.push([3,5,,6]),[4,this.reconnectCallback()];case 4:return r.sent()?[2]:(this.reconnectDisplay.rejected(),[2]);case 5:return n=r.sent(),this.logger.log(s.LogLevel.Error,n),[3,6];case 6:return t++,[3,1];case 7:return this.reconnectDisplay.failed(),[2]}})})},e.prototype.delay=function(e){return new Promise(function(t){return setTimeout(t,e)})},e}()},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var i=n(17),a=function(){function e(e,t,n){var a=this;this.document=t,this.logger=n,this.addedToDom=!1,this.modal=this.document.createElement("div"),this.modal.id=e;this.modal.style.cssText=["position: fixed","top: 0","right: 0","bottom: 0","left: 0","z-index: 1000","display: none","overflow: hidden","background-color: #fff","opacity: 0.8","text-align: center","font-weight: bold"].join(";"),this.modal.innerHTML='<h5 style="margin-top: 20px"></h5><button style="margin:5px auto 5px">Retry</button><p>Alternatively, <a href>reload</a></p>',this.message=this.modal.querySelector("h5"),this.button=this.modal.querySelector("button"),this.reloadParagraph=this.modal.querySelector("p"),this.button.addEventListener("click",function(){return r(a,void 0,void 0,function(){var e;return o(this,function(t){switch(t.label){case 0:this.show(),t.label=1;case 1:return t.trys.push([1,3,,4]),[4,window.Blazor.reconnect()];case 2:return t.sent()||this.rejected(),[3,4];case 3:return e=t.sent(),this.logger.log(i.LogLevel.Error,e),this.failed(),[3,4];case 4:return[2]}})})}),this.reloadParagraph.querySelector("a").addEventListener("click",function(){return location.reload()})}return e.prototype.show=function(){this.addedToDom||(this.addedToDom=!0,this.document.body.appendChild(this.modal)),this.modal.style.display="block",this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.textContent="Attempting to reconnect to the server..."},e.prototype.hide=function(){this.modal.style.display="none"},e.prototype.failed=function(){this.button.style.display="block",this.reloadParagraph.style.display="none",this.message.innerHTML="Reconnection failed. Try <a href>reloading</a> the page if you're unable to reconnect.",this.message.querySelector("a").addEventListener("click",function(){return location.reload()})},e.prototype.rejected=function(){this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.innerHTML="Could not reconnect to the server. <a href>Reload</a> the page to restore functionality.",this.message.querySelector("a").addEventListener("click",function(){return location.reload()})},e}();t.DefaultReconnectDisplay=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.dialog=e}return e.prototype.show=function(){this.removeClasses(),this.dialog.classList.add(e.ShowClassName)},e.prototype.hide=function(){this.removeClasses(),this.dialog.classList.add(e.HideClassName)},e.prototype.failed=function(){this.removeClasses(),this.dialog.classList.add(e.FailedClassName)},e.prototype.rejected=function(){this.removeClasses(),this.dialog.classList.add(e.RejectedClassName)},e.prototype.removeClasses=function(){this.dialog.classList.remove(e.ShowClassName,e.HideClassName,e.FailedClassName,e.RejectedClassName)},e.ShowClassName="components-reconnect-show",e.HideClassName="components-reconnect-hide",e.FailedClassName="components-reconnect-failed",e.RejectedClassName="components-reconnect-rejected",e}();t.UserSpecifiedDisplay=r},function(e,t,n){"use strict";n.r(t);var r=n(6),o=n(12),i=n(2),a=function(){function e(){}return e.write=function(e){var t=e.byteLength||e.length,n=[];do{var r=127&t;(t>>=7)>0&&(r|=128),n.push(r)}while(t>0);t=e.byteLength||e.length;var o=new Uint8Array(n.length+t);return o.set(n,0),o.set(e,n.length),o.buffer},e.parse=function(e){for(var t=[],n=new Uint8Array(e),r=[0,7,14,21,28],o=0;o<e.byteLength;){var i=0,a=0,s=void 0;do{a|=(127&(s=n[o+i]))<<r[i],i++}while(i<Math.min(5,e.byteLength-o)&&0!=(128&s));if(0!=(128&s)&&i<5)throw new Error("Cannot read message size.");if(5===i&&s>7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+i+a))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+i,o+i+a):n.subarray(o+i,o+i+a)),o=o+i+a}return t},e}();var s=new Uint8Array([145,i.MessageType.Ping]),c=function(){function e(){this.name="messagepack",this.version=1,this.transferFormat=i.TransferFormat.Binary,this.errorResult=1,this.voidResult=2,this.nonVoidResult=3}return e.prototype.parseMessages=function(e,t){if(!(e instanceof r.Buffer||(n=e,n&&"undefined"!=typeof ArrayBuffer&&(n instanceof ArrayBuffer||n.constructor&&"ArrayBuffer"===n.constructor.name))))throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer or Buffer.");var n;null===t&&(t=i.NullLogger.instance);for(var o=[],s=0,c=a.parse(e);s<c.length;s++){var u=c[s],l=this.parseMessage(u,t);l&&o.push(l)}return o},e.prototype.writeMessage=function(e){switch(e.type){case i.MessageType.Invocation:return this.writeInvocation(e);case i.MessageType.StreamInvocation:return this.writeStreamInvocation(e);case i.MessageType.StreamItem:return this.writeStreamItem(e);case i.MessageType.Completion:return this.writeCompletion(e);case i.MessageType.Ping:return a.write(s);case i.MessageType.CancelInvocation:return this.writeCancelInvocation(e);default:throw new Error("Invalid message type.")}},e.prototype.parseMessage=function(e,t){if(0===e.length)throw new Error("Invalid payload.");var n=o().decode(r.Buffer.from(e));if(0===n.length||!(n instanceof Array))throw new Error("Invalid payload.");var a=n[0];switch(a){case i.MessageType.Invocation:return this.createInvocationMessage(this.readHeaders(n),n);case i.MessageType.StreamItem:return this.createStreamItemMessage(this.readHeaders(n),n);case i.MessageType.Completion:return this.createCompletionMessage(this.readHeaders(n),n);case i.MessageType.Ping:return this.createPingMessage(n);case i.MessageType.Close:return this.createCloseMessage(n);default:return t.log(i.LogLevel.Information,"Unknown message type '"+a+"' ignored."),null}},e.prototype.createCloseMessage=function(e){if(e.length<2)throw new Error("Invalid payload for Close message.");return{error:e[1],type:i.MessageType.Close}},e.prototype.createPingMessage=function(e){if(e.length<1)throw new Error("Invalid payload for Ping message.");return{type:i.MessageType.Ping}},e.prototype.createInvocationMessage=function(e,t){if(t.length<5)throw new Error("Invalid payload for Invocation message.");var n=t[2];return n?{arguments:t[4],headers:e,invocationId:n,streamIds:[],target:t[3],type:i.MessageType.Invocation}:{arguments:t[4],headers:e,streamIds:[],target:t[3],type:i.MessageType.Invocation}},e.prototype.createStreamItemMessage=function(e,t){if(t.length<4)throw new Error("Invalid payload for StreamItem message.");return{headers:e,invocationId:t[2],item:t[3],type:i.MessageType.StreamItem}},e.prototype.createCompletionMessage=function(e,t){if(t.length<4)throw new Error("Invalid payload for Completion message.");var n,r,o=t[3];if(o!==this.voidResult&&t.length<5)throw new Error("Invalid payload for Completion message.");switch(o){case this.errorResult:n=t[4];break;case this.nonVoidResult:r=t[4]}return{error:n,headers:e,invocationId:t[2],result:r,type:i.MessageType.Completion}},e.prototype.writeInvocation=function(e){var t=o().encode([i.MessageType.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments,e.streamIds]);return a.write(t.slice())},e.prototype.writeStreamInvocation=function(e){var t=o().encode([i.MessageType.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments,e.streamIds]);return a.write(t.slice())},e.prototype.writeStreamItem=function(e){var t=o().encode([i.MessageType.StreamItem,e.headers||{},e.invocationId,e.item]);return a.write(t.slice())},e.prototype.writeCompletion=function(e){var t,n=o(),r=e.error?this.errorResult:e.result?this.nonVoidResult:this.voidResult;switch(r){case this.errorResult:t=n.encode([i.MessageType.Completion,e.headers||{},e.invocationId,r,e.error]);break;case this.voidResult:t=n.encode([i.MessageType.Completion,e.headers||{},e.invocationId,r]);break;case this.nonVoidResult:t=n.encode([i.MessageType.Completion,e.headers||{},e.invocationId,r,e.result])}return a.write(t.slice())},e.prototype.writeCancelInvocation=function(e){var t=o().encode([i.MessageType.CancelInvocation,e.headers||{},e.invocationId]);return a.write(t.slice())},e.prototype.readHeaders=function(e){var t=e[1];if("object"!=typeof t)throw new Error("Invalid headers.");return t},e}();n.d(t,"VERSION",function(){return u}),n.d(t,"MessagePackHubProtocol",function(){return c});var u="3.1.0-dev"}]); diff --git a/src/Components/Web/src/Forms/InputBase.cs b/src/Components/Web/src/Forms/InputBase.cs index f60c330326..af2dee2ead 100644 --- a/src/Components/Web/src/Forms/InputBase.cs +++ b/src/Components/Web/src/Forms/InputBase.cs @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Components.Forms } /// <summary> - /// Formats the value as a string. Derived classes can override this to determine the formating used for <see cref="CurrentValueAsString"/>. + /// Formats the value as a string. Derived classes can override this to determine the formatting used for <see cref="CurrentValueAsString"/>. /// </summary> /// <param name="value">The value to format.</param> /// <returns>A string representation of the value.</returns> diff --git a/src/Components/Web/src/Forms/InputNumber.cs b/src/Components/Web/src/Forms/InputNumber.cs index 4f0377ceed..7c8eec1a23 100644 --- a/src/Components/Web/src/Forms/InputNumber.cs +++ b/src/Components/Web/src/Forms/InputNumber.cs @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Components.Forms } /// <summary> - /// Formats the value as a string. Derived classes can override this to determine the formating used for <c>CurrentValueAsString</c>. + /// Formats the value as a string. Derived classes can override this to determine the formatting used for <c>CurrentValueAsString</c>. /// </summary> /// <param name="value">The value to format.</param> /// <returns>A string representation of the value.</returns> diff --git a/src/Components/Web/test/Forms/InputBaseTest.cs b/src/Components/Web/test/Forms/InputBaseTest.cs index 570b0f9283..9f43181465 100644 --- a/src/Components/Web/test/Forms/InputBaseTest.cs +++ b/src/Components/Web/test/Forms/InputBaseTest.cs @@ -212,7 +212,7 @@ namespace Microsoft.AspNetCore.Components.Forms }; var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty); - // Act/Assert: Initally, it's valid and unmodified + // Act/Assert: Initially, it's valid and unmodified var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); Assert.Equal("valid", inputComponent.CssClass); // no Class was specified diff --git a/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs index ed045a6383..cdca53efaf 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests [Theory] [InlineData("en-US")] [InlineData("fr-FR")] - public void CanSetCultureAndParseCultueSensitiveNumbersAndDates(string culture) + public void CanSetCultureAndParseCultureSensitiveNumbersAndDates(string culture) { var cultureInfo = CultureInfo.GetCultureInfo(culture); SetCulture(culture); diff --git a/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor b/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor index 13256fb7bc..77fbab5eb5 100644 --- a/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor @@ -18,7 +18,7 @@ <p id="errormessage-failure">Error = @errorFailure</p> <button id="triggerjsinterop-malformed" @onclick="@TriggerJSInterop">Trigger malformed JS interop callback</button> -<button id="triggerjsinterop-success" @onclick="@TriggerJSInteropSuccess">Trigger successfull JS interop callback</button> +<button id="triggerjsinterop-success" @onclick="@TriggerJSInteropSuccess">Trigger successful JS interop callback</button> <button id="triggerjsinterop-failure" @onclick="@TriggerJSInteropFailure">Trigger error JS interop callback</button> <button id="event-handler-throw-sync" @onclick="@TriggerSyncException">Trigger sync exception</button> diff --git a/src/DataProtection/AzureStorage/src/Microsoft.AspNetCore.DataProtection.AzureStorage.csproj b/src/DataProtection/AzureStorage/src/Microsoft.AspNetCore.DataProtection.AzureStorage.csproj index b2e913f352..40698c9d33 100644 --- a/src/DataProtection/AzureStorage/src/Microsoft.AspNetCore.DataProtection.AzureStorage.csproj +++ b/src/DataProtection/AzureStorage/src/Microsoft.AspNetCore.DataProtection.AzureStorage.csproj @@ -15,7 +15,7 @@ <Reference Include="Microsoft.Azure.Storage.Blob" /> </ItemGroup> - <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.1'"> + <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'"> <!-- This dependency was replaced by Microsoft.Azure.Storage.Blob between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <SuppressBaselineReference Include="WindowsAzure.Storage" /> <Reference Include="Microsoft.Data.OData" /> diff --git a/src/DataProtection/README.md b/src/DataProtection/README.md index cd58074d9e..4c558753b0 100644 --- a/src/DataProtection/README.md +++ b/src/DataProtection/README.md @@ -1,7 +1,7 @@ DataProtection ============== -Data Protection APIs for protecting and unprotecting data. You can find documentation for Data Protection in the [ASP.NET Core Documentation](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/). +Data Protection APIs for protecting and unprotecting data. You can find documentation for Data Protection in the [ASP.NET Core Documentation](https://docs.microsoft.com/aspnet/core/security/data-protection/). ## Community Maintained Data Protection Providers & Projects diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs index 79ef5dfc33..da51bb4a3c 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs @@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Tests var applicationName = "CreateDefaultBuilderApp"; var deploymentParameters = new DeploymentParameters(Path.Combine(GetTestSitesPath(), applicationName), ServerType.IISExpress, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) { - TargetFramework = "netcoreapp3.1", + TargetFramework = "netcoreapp5.0", HostingModel = HostingModel.InProcess }; @@ -213,7 +213,7 @@ namespace Microsoft.AspNetCore.Tests { var deploymentParameters = new DeploymentParameters(Path.Combine(GetTestSitesPath(), applicationName), ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) { - TargetFramework = "netcoreapp3.1", + TargetFramework = "netcoreapp5.0", }; if (setTestEnvVars) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplication.cs b/src/Hosting/Hosting/src/Internal/HostingApplication.cs index c64426c0db..84363221ba 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplication.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplication.cs @@ -114,6 +114,7 @@ namespace Microsoft.AspNetCore.Hosting public HttpContext HttpContext { get; set; } public IDisposable Scope { get; set; } public Activity Activity { get; set; } + internal HostingRequestStartingLog StartLog { get; set; } public long StartTimestamp { get; set; } internal bool HasDiagnosticListener { get; set; } @@ -125,6 +126,7 @@ namespace Microsoft.AspNetCore.Hosting Scope = null; Activity = null; + StartLog = null; StartTimestamp = 0; HasDiagnosticListener = false; diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 65730d67ff..385bcbf46d 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Hosting } // Non-inline - LogRequestStarting(httpContext); + LogRequestStarting(context); } } context.StartTimestamp = startTimestamp; @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Hosting { currentTimestamp = Stopwatch.GetTimestamp(); // Non-inline - LogRequestFinished(httpContext, startTimestamp, currentTimestamp); + LogRequestFinished(context, startTimestamp, currentTimestamp); } if (_diagnosticListener.IsEnabled()) @@ -167,30 +167,34 @@ namespace Microsoft.AspNetCore.Hosting } [MethodImpl(MethodImplOptions.NoInlining)] - private void LogRequestStarting(HttpContext httpContext) + private void LogRequestStarting(HostingApplication.Context context) { // IsEnabled is checked in the caller, so if we are here just log + var startLog = new HostingRequestStartingLog(context.HttpContext); + context.StartLog = startLog; + _logger.Log( logLevel: LogLevel.Information, eventId: LoggerEventIds.RequestStarting, - state: new HostingRequestStartingLog(httpContext), + state: startLog, exception: null, formatter: HostingRequestStartingLog.Callback); } [MethodImpl(MethodImplOptions.NoInlining)] - private void LogRequestFinished(HttpContext httpContext, long startTimestamp, long currentTimestamp) + private void LogRequestFinished(HostingApplication.Context context, long startTimestamp, long currentTimestamp) { // IsEnabled isn't checked in the caller, startTimestamp > 0 is used as a fast proxy check - // but that may be because diagnostics are enabled, which also uses startTimestamp, so check here - if (_logger.IsEnabled(LogLevel.Information)) + // but that may be because diagnostics are enabled, which also uses startTimestamp, + // so check if we logged the start event + if (context.StartLog != null) { var elapsed = new TimeSpan((long)(TimestampToTicks * (currentTimestamp - startTimestamp))); _logger.Log( logLevel: LogLevel.Information, eventId: LoggerEventIds.RequestFinished, - state: new HostingRequestFinishedLog(httpContext, elapsed), + state: new HostingRequestFinishedLog(context, elapsed), exception: null, formatter: HostingRequestFinishedLog.Callback); } diff --git a/src/Hosting/Hosting/src/Internal/HostingRequestFinishedLog.cs b/src/Hosting/Hosting/src/Internal/HostingRequestFinishedLog.cs index 63fd5f0921..09a132851d 100644 --- a/src/Hosting/Hosting/src/Internal/HostingRequestFinishedLog.cs +++ b/src/Hosting/Hosting/src/Internal/HostingRequestFinishedLog.cs @@ -9,51 +9,56 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Hosting { + using static HostingRequestStartingLog; + internal class HostingRequestFinishedLog : IReadOnlyList<KeyValuePair<string, object>> { internal static readonly Func<object, Exception, string> Callback = (state, exception) => ((HostingRequestFinishedLog)state).ToString(); - private readonly HttpContext _httpContext; - private readonly TimeSpan _elapsed; + private readonly HostingApplication.Context _context; private string _cachedToString; + public TimeSpan Elapsed { get; } - public int Count => 3; + public int Count => 11; public KeyValuePair<string, object> this[int index] { get { - switch (index) + var request = _context.HttpContext.Request; + var response = _context.HttpContext.Response; + + return index switch { - case 0: - return new KeyValuePair<string, object>("ElapsedMilliseconds", _elapsed.TotalMilliseconds); - case 1: - return new KeyValuePair<string, object>("StatusCode", _httpContext.Response.StatusCode); - case 2: - return new KeyValuePair<string, object>("ContentType", _httpContext.Response.ContentType); - default: - throw new IndexOutOfRangeException(nameof(index)); - } + 0 => new KeyValuePair<string, object>("ElapsedMilliseconds", Elapsed.TotalMilliseconds), + 1 => new KeyValuePair<string, object>(nameof(response.StatusCode), response.StatusCode), + 2 => new KeyValuePair<string, object>(nameof(response.ContentType), response.ContentType), + 3 => new KeyValuePair<string, object>(nameof(response.ContentLength), response.ContentLength), + 4 => new KeyValuePair<string, object>(nameof(request.Protocol), request.Protocol), + 5 => new KeyValuePair<string, object>(nameof(request.Method), request.Method), + 6 => new KeyValuePair<string, object>(nameof(request.Scheme), request.Scheme), + 7 => new KeyValuePair<string, object>(nameof(request.Host), request.Host.Value), + 8 => new KeyValuePair<string, object>(nameof(request.PathBase), request.PathBase.Value), + 9 => new KeyValuePair<string, object>(nameof(request.Path), request.Path.Value), + 10 => new KeyValuePair<string, object>(nameof(request.QueryString), request.QueryString.Value), + _ => throw new IndexOutOfRangeException(nameof(index)), + }; } } - public HostingRequestFinishedLog(HttpContext httpContext, TimeSpan elapsed) + public HostingRequestFinishedLog(HostingApplication.Context context, TimeSpan elapsed) { - _httpContext = httpContext; - _elapsed = elapsed; + _context = context; + Elapsed = elapsed; } public override string ToString() { if (_cachedToString == null) { - _cachedToString = string.Format( - CultureInfo.InvariantCulture, - "Request finished in {0}ms {1} {2}", - _elapsed.TotalMilliseconds, - _httpContext.Response.StatusCode, - _httpContext.Response.ContentType); + var response = _context.HttpContext.Response; + _cachedToString = $"Request finished {_context.StartLog.ToStringWithoutPreamble()} - {response.StatusCode.ToString(CultureInfo.InvariantCulture)} {ValueOrEmptyMarker(response.ContentLength)} {EscapedValueOrEmptyMarker(response.ContentType)} {Elapsed.TotalMilliseconds.ToString("0.0000", CultureInfo.InvariantCulture)}ms"; } return _cachedToString; diff --git a/src/Hosting/Hosting/src/Internal/HostingRequestStartingLog.cs b/src/Hosting/Hosting/src/Internal/HostingRequestStartingLog.cs index 279fa06aed..3a7586b1c9 100644 --- a/src/Hosting/Hosting/src/Internal/HostingRequestStartingLog.cs +++ b/src/Hosting/Hosting/src/Internal/HostingRequestStartingLog.cs @@ -5,12 +5,16 @@ using System; using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Net; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Hosting { internal class HostingRequestStartingLog : IReadOnlyList<KeyValuePair<string, object>> { + private const string LogPreamble = "Request starting "; + private const string EmptyEntry = "-"; + internal static readonly Func<object, Exception, string> Callback = (state, exception) => ((HostingRequestStartingLog)state).ToString(); private readonly HttpRequest _request; @@ -19,35 +23,19 @@ namespace Microsoft.AspNetCore.Hosting public int Count => 9; - public KeyValuePair<string, object> this[int index] + public KeyValuePair<string, object> this[int index] => index switch { - get - { - switch (index) - { - case 0: - return new KeyValuePair<string, object>("Protocol", _request.Protocol); - case 1: - return new KeyValuePair<string, object>("Method", _request.Method); - case 2: - return new KeyValuePair<string, object>("ContentType", _request.ContentType); - case 3: - return new KeyValuePair<string, object>("ContentLength", _request.ContentLength); - case 4: - return new KeyValuePair<string, object>("Scheme", _request.Scheme); - case 5: - return new KeyValuePair<string, object>("Host", _request.Host.ToString()); - case 6: - return new KeyValuePair<string, object>("PathBase", _request.PathBase.ToString()); - case 7: - return new KeyValuePair<string, object>("Path", _request.Path.ToString()); - case 8: - return new KeyValuePair<string, object>("QueryString", _request.QueryString.ToString()); - default: - throw new IndexOutOfRangeException(nameof(index)); - } - } - } + 0 => new KeyValuePair<string, object>(nameof(_request.Protocol), _request.Protocol), + 1 => new KeyValuePair<string, object>(nameof(_request.Method), _request.Method), + 2 => new KeyValuePair<string, object>(nameof(_request.ContentType), _request.ContentType), + 3 => new KeyValuePair<string, object>(nameof(_request.ContentLength), _request.ContentLength), + 4 => new KeyValuePair<string, object>(nameof(_request.Scheme), _request.Scheme), + 5 => new KeyValuePair<string, object>(nameof(_request.Host), _request.Host.Value), + 6 => new KeyValuePair<string, object>(nameof(_request.PathBase), _request.PathBase.Value), + 7 => new KeyValuePair<string, object>(nameof(_request.Path), _request.Path.Value), + 8 => new KeyValuePair<string, object>(nameof(_request.QueryString), _request.QueryString.Value), + _ => throw new IndexOutOfRangeException(nameof(index)), + }; public HostingRequestStartingLog(HttpContext httpContext) { @@ -58,18 +46,8 @@ namespace Microsoft.AspNetCore.Hosting { if (_cachedToString == null) { - _cachedToString = string.Format( - CultureInfo.InvariantCulture, - "Request starting {0} {1} {2}://{3}{4}{5}{6} {7} {8}", - _request.Protocol, - _request.Method, - _request.Scheme, - _request.Host.Value, - _request.PathBase.Value, - _request.Path.Value, - _request.QueryString.Value, - _request.ContentType, - _request.ContentLength); + var request = _request; + _cachedToString = $"{LogPreamble}{request.Protocol} {request.Method} {request.Scheme}://{request.Host.Value}{request.PathBase.Value}{request.Path.Value}{request.QueryString.Value} {EscapedValueOrEmptyMarker(request.ContentType)} {ValueOrEmptyMarker(request.ContentLength)}"; ; } return _cachedToString; @@ -87,5 +65,15 @@ namespace Microsoft.AspNetCore.Hosting { return GetEnumerator(); } + + internal string ToStringWithoutPreamble() + => ToString().Substring(LogPreamble.Length); + + internal static string EscapedValueOrEmptyMarker(string potentialValue) + // Encode space as + + => potentialValue?.Length > 0 ? potentialValue.Replace(' ', '+') : EmptyEntry; + + internal static string ValueOrEmptyMarker<T>(T? potentialValue) where T : struct, IFormattable + => potentialValue?.ToString(null, CultureInfo.InvariantCulture) ?? EmptyEntry; } } diff --git a/src/Hosting/Hosting/src/Internal/WebHost.cs b/src/Hosting/Hosting/src/Internal/WebHost.cs index 6e976959e7..ec0d6bdd8b 100644 --- a/src/Hosting/Hosting/src/Internal/WebHost.cs +++ b/src/Hosting/Hosting/src/Internal/WebHost.cs @@ -355,7 +355,7 @@ namespace Microsoft.AspNetCore.Hosting public void Dispose() { - DisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + DisposeAsync().GetAwaiter().GetResult(); } public async ValueTask DisposeAsync() diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs b/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs index 5eea2b8ce3..e1608bba80 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace Microsoft.AspNetCore.Server.IntegrationTesting diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/IWebHostExtensions.cs b/src/Hosting/Server.IntegrationTesting/src/Common/IWebHostExtensions.cs deleted file mode 100644 index 732a598ab8..0000000000 --- a/src/Hosting/Server.IntegrationTesting/src/Common/IWebHostExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.AspNetCore.Hosting.Server.Features; -using System.Linq; - -namespace Microsoft.AspNetCore.Hosting -{ - public static class IWebHostExtensions - { - public static string GetAddress(this IWebHost host) - { - return host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First(); - } - } -} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/TestPortHelper.cs b/src/Hosting/Server.IntegrationTesting/src/Common/TestPortHelper.cs index 7129ff73d3..b8688dec04 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/TestPortHelper.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/TestPortHelper.cs @@ -56,34 +56,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common } } } - - private const int BasePort = 5001; - private const int MaxPort = 8000; - private static int NextPort = BasePort; - - // GetNextPort doesn't check for HttpSys urlacls. - public static int GetNextHttpSysPort(string scheme) - { - while (NextPort < MaxPort) - { - var port = NextPort++; - - using (var server = new HttpListener()) - { - server.Prefixes.Add($"{scheme}://localhost:{port}/"); - try - { - server.Start(); - server.Stop(); - return port; - } - catch (HttpListenerException) - { - } - } - } - NextPort = BasePort; - throw new Exception("Failed to locate a free port."); - } } } diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs b/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs index aaac5b88a7..4899fbea4f 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs @@ -1,7 +1,9 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; + namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common { public static class TestUriHelper @@ -34,7 +36,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common } else if (serverType == ServerType.HttpSys) { - return new UriBuilder(scheme, "localhost", TestPortHelper.GetNextHttpSysPort(scheme)).Uri; + Debug.Assert(scheme == "http", "Https not supported"); + return new UriBuilder(scheme, "localhost", 0).Uri; } else { diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs b/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs index b828a5868e..92cea4fb37 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting public const string NetCoreApp22 = "netcoreapp2.2"; public const string NetCoreApp30 = "netcoreapp3.0"; public const string NetCoreApp31 = "netcoreapp3.1"; + public const string NetCoreApp50 = "netcoreapp5.0"; public static bool Matches(string tfm1, string tfm2) { diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs index d5c0dc02ca..262ff80ce8 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs index 06923f7c46..e6e724c98c 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs @@ -22,7 +22,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$"); private const string ApplicationStartedMessage = "Application started. Press Ctrl+C to shut down."; - private const int RetryCount = 5; public Process HostProcess { get; private set; } public SelfHostDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) @@ -56,33 +55,23 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting DotnetPublish(); } - // Launch the host process. - for (var i = 0; i < RetryCount; i++) - { - var hintUrl = TestUriHelper.BuildTestUri( - DeploymentParameters.ServerType, - DeploymentParameters.Scheme, - DeploymentParameters.ApplicationBaseUriHint, - DeploymentParameters.StatusMessagesEnabled); - var (actualUrl, hostExitToken) = await StartSelfHostAsync(hintUrl); - - if (DeploymentParameters.ServerType == ServerType.HttpSys && hostExitToken.IsCancellationRequested) - { - // Retry HttpSys deployments due to port conflicts. - continue; - } + var hintUrl = TestUriHelper.BuildTestUri( + DeploymentParameters.ServerType, + DeploymentParameters.Scheme, + DeploymentParameters.ApplicationBaseUriHint, + DeploymentParameters.StatusMessagesEnabled); - Logger.LogInformation("Application ready at URL: {appUrl}", actualUrl); + // Launch the host process. + var (actualUrl, hostExitToken) = await StartSelfHostAsync(hintUrl); - return new DeploymentResult( - LoggerFactory, - DeploymentParameters, - applicationBaseUri: actualUrl.ToString(), - contentRoot: DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath, - hostShutdownToken: hostExitToken); - } + Logger.LogInformation("Application ready at URL: {appUrl}", actualUrl); - throw new Exception($"Failed to start Self hosted application after {RetryCount} retries."); + return new DeploymentResult( + LoggerFactory, + DeploymentParameters, + applicationBaseUri: actualUrl.ToString(), + contentRoot: DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath, + hostShutdownToken: hostExitToken); } } @@ -176,6 +165,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id); // If TrySetResult was called above, this will just silently fail to set the new state, which is what we want + started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {HostProcess.ExitCode}")); + TriggerHostShutdown(hostExitTokenSource); }; @@ -187,6 +178,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString()); } + if (HostProcess.HasExited) { Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode); diff --git a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj index 8edf4ff3f0..ba625f4332 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj +++ b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <Description>ASP.NET Core helpers to deploy applications to IIS Express, IIS, WebListener and Kestrel for testing.</Description> @@ -19,7 +19,6 @@ </ItemGroup> <ItemGroup> - <Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" /> <Reference Include="Microsoft.AspNetCore.Testing" /> <Reference Include="Microsoft.Extensions.FileProviders.Embedded" /> <Reference Include="Microsoft.Extensions.Logging" /> diff --git a/src/Hosting/build.cmd b/src/Hosting/build.cmd deleted file mode 100644 index 2406296662..0000000000 --- a/src/Hosting/build.cmd +++ /dev/null @@ -1,3 +0,0 @@ -@ECHO OFF -SET RepoRoot=%~dp0..\.. -%RepoRoot%\build.cmd -projects %~dp0**\*.*proj %* diff --git a/src/Hosting/test/FunctionalTests/ShutdownTests.cs b/src/Hosting/test/FunctionalTests/ShutdownTests.cs index deafac6c18..46007e6a99 100644 --- a/src/Hosting/test/FunctionalTests/ShutdownTests.cs +++ b/src/Hosting/test/FunctionalTests/ShutdownTests.cs @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests RuntimeArchitecture.x64) { EnvironmentName = "Shutdown", - TargetFramework = Tfm.NetCoreApp31, + TargetFramework = Tfm.NetCoreApp50, ApplicationType = ApplicationType.Portable, PublishApplicationBeforeDeployment = true, StatusMessagesEnabled = false diff --git a/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs b/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs index 5d5044eacc..8747a75f26 100644 --- a/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs +++ b/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests public WebHostBuilderTests(ITestOutputHelper output) : base(output) { } public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.Kestrel) - .WithTfms(Tfm.NetCoreApp31); + .WithTfms(Tfm.NetCoreApp50); [ConditionalTheory] [MemberData(nameof(TestVariants))] diff --git a/src/Http/Authentication.Core/src/AuthenticationService.cs b/src/Http/Authentication.Core/src/AuthenticationService.cs index 7efce69ce8..9cc0807539 100644 --- a/src/Http/Authentication.Core/src/AuthenticationService.cs +++ b/src/Http/Authentication.Core/src/AuthenticationService.cs @@ -253,7 +253,7 @@ namespace Microsoft.AspNetCore.Authentication var schemes = await GetAllSignInSchemeNames(); // CookieAuth is the only implementation of sign-in. - var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; + var footer = $" Did you forget to call AddAuthentication().AddCookie(\"{scheme}\",...)?"; if (string.IsNullOrEmpty(schemes)) { @@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.Authentication { // CookieAuth is the only implementation of sign-in. return new InvalidOperationException(mismatchError - + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?"); + + $"Did you forget to call AddAuthentication().AddCookie(\"Cookies\") and SignInAsync(\"Cookies\",...)?"); } return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}."); @@ -292,7 +292,7 @@ namespace Microsoft.AspNetCore.Authentication { var schemes = await GetAllSignOutSchemeNames(); - var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; + var footer = $" Did you forget to call AddAuthentication().AddCookie(\"{scheme}\",...)?"; if (string.IsNullOrEmpty(schemes)) { @@ -314,7 +314,7 @@ namespace Microsoft.AspNetCore.Authentication { // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. return new InvalidOperationException(mismatchError - + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?"); + + $"Did you forget to call AddAuthentication().AddCookie(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?"); } return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}."); diff --git a/src/Http/Http/src/Features/RequestServicesFeature.cs b/src/Http/Http/src/Features/RequestServicesFeature.cs index 6585127367..5a1a714998 100644 --- a/src/Http/Http/src/Features/RequestServicesFeature.cs +++ b/src/Http/Http/src/Features/RequestServicesFeature.cs @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Http.Features public void Dispose() { - DisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + DisposeAsync().GetAwaiter().GetResult(); } } } diff --git a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs index fd9c993bad..da24578135 100644 --- a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs @@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.WebUtilities { if (Disposed) { - throw new ObjectDisposedException(nameof(FileBufferingReadStream)); + throw new ObjectDisposedException(nameof(FileBufferingWriteStream)); } } diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ExternalLogin.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ExternalLogin.cshtml index e0053be7fd..f73e2fe556 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ExternalLogin.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ExternalLogin.cshtml @@ -17,7 +17,7 @@ <div class="row"> <div class="col-md-4"> <form asp-page-handler="Confirmation" asp-route-returnUrl="@Model.ReturnUrl" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ForgotPassword.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ForgotPassword.cshtml index b938724210..7541125984 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ForgotPassword.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ForgotPassword.cshtml @@ -10,7 +10,7 @@ <div class="row"> <div class="col-md-4"> <form method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Login.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Login.cshtml index a58edae8a8..8ebfba2d1b 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Login.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Login.cshtml @@ -12,7 +12,7 @@ <form id="account" method="post"> <h4>Use a local account to log in.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/LoginWith2fa.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/LoginWith2fa.cshtml index 94242c929e..28b23ae8e1 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/LoginWith2fa.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/LoginWith2fa.cshtml @@ -11,7 +11,7 @@ <div class="col-md-4"> <form method="post" asp-route-returnUrl="@Model.ReturnUrl"> <input asp-for="RememberMe" type="hidden" /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.TwoFactorCode"></label> <input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/LoginWithRecoveryCode.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/LoginWithRecoveryCode.cshtml index abaad2a4d4..9f0e526bcf 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/LoginWithRecoveryCode.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/LoginWithRecoveryCode.cshtml @@ -13,7 +13,7 @@ <div class="row"> <div class="col-md-4"> <form method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.RecoveryCode"></label> <input asp-for="Input.RecoveryCode" class="form-control" autocomplete="off" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/ChangePassword.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/ChangePassword.cshtml index 701c1ea457..0cdc9c97a7 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/ChangePassword.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/ChangePassword.cshtml @@ -10,7 +10,7 @@ <div class="row"> <div class="col-md-6"> <form id="change-password-form" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.OldPassword"></label> <input asp-for="Input.OldPassword" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/DeletePersonalData.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/DeletePersonalData.cshtml index e1a2b7a8a2..4939c7fd5a 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/DeletePersonalData.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/DeletePersonalData.cshtml @@ -16,7 +16,7 @@ <div> <form id="delete-user" method="post" class="form-group"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> @if (Model.RequirePassword) { <div class="form-group"> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/Index.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/Index.cshtml index 19ac1a296e..2adbdecf6f 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/Index.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/Index.cshtml @@ -10,7 +10,7 @@ <div class="row"> <div class="col-md-6"> <form id="profile-form" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Username"></label> <input asp-for="Username" class="form-control" disabled /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/SetPassword.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/SetPassword.cshtml index 31632c3860..e82ce19df5 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/SetPassword.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Manage/SetPassword.cshtml @@ -14,7 +14,7 @@ <div class="row"> <div class="col-md-6"> <form id="set-password-form" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.NewPassword"></label> <input asp-for="Input.NewPassword" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Register.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Register.cshtml index cf6a7c6e96..78415afe1a 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Register.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Register.cshtml @@ -11,7 +11,7 @@ <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post"> <h4>Create a new account.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ResetPassword.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ResetPassword.cshtml index 5ccb61edcf..97629b27cd 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ResetPassword.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V3/Account/ResetPassword.cshtml @@ -10,7 +10,7 @@ <div class="row"> <div class="col-md-4"> <form method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Input.Code" type="hidden" /> <div class="form-group"> <label asp-for="Input.Email"></label> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml index d92664c31c..7579138faa 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml @@ -17,7 +17,7 @@ <div class="row"> <div class="col-md-4"> <form asp-page-handler="Confirmation" asp-route-returnUrl="@Model.ReturnUrl" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ForgotPassword.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ForgotPassword.cshtml index d3eb7ce65d..4570844df8 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ForgotPassword.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ForgotPassword.cshtml @@ -10,7 +10,7 @@ <div class="row"> <div class="col-md-4"> <form method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Login.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Login.cshtml index b98655e0f6..7db2738277 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Login.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Login.cshtml @@ -12,7 +12,7 @@ <form id="account" method="post"> <h4>Use a local account to log in.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWith2fa.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWith2fa.cshtml index b5508902f6..5cc2aa2d13 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWith2fa.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWith2fa.cshtml @@ -11,7 +11,7 @@ <div class="col-md-4"> <form method="post" asp-route-returnUrl="@Model.ReturnUrl"> <input asp-for="RememberMe" type="hidden" /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.TwoFactorCode"></label> <input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWithRecoveryCode.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWithRecoveryCode.cshtml index 957f72b00f..5947903f87 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWithRecoveryCode.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/LoginWithRecoveryCode.cshtml @@ -13,7 +13,7 @@ <div class="row"> <div class="col-md-4"> <form method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.RecoveryCode"></label> <input asp-for="Input.RecoveryCode" class="form-control" autocomplete="off" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/ChangePassword.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/ChangePassword.cshtml index 07e23b117c..4d5dda19ec 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/ChangePassword.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/ChangePassword.cshtml @@ -10,7 +10,7 @@ <div class="row"> <div class="col-md-6"> <form id="change-password-form" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.OldPassword"></label> <input asp-for="Input.OldPassword" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/DeletePersonalData.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/DeletePersonalData.cshtml index e1896604ff..abcd16a82f 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/DeletePersonalData.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/DeletePersonalData.cshtml @@ -15,7 +15,7 @@ <div> <form id="delete-user" method="post" class="form-group"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> @if (Model.RequirePassword) { <div class="form-group"> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/Index.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/Index.cshtml index dd25959817..1c125b18f7 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/Index.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/Index.cshtml @@ -10,7 +10,7 @@ <div class="row"> <div class="col-md-6"> <form id="profile-form" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Username"></label> <input asp-for="Username" class="form-control" disabled /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/SetPassword.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/SetPassword.cshtml index 2a42c34f36..593526746e 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/SetPassword.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/SetPassword.cshtml @@ -14,7 +14,7 @@ <div class="row"> <div class="col-md-6"> <form id="set-password-form" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.NewPassword"></label> <input asp-for="Input.NewPassword" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml index 99620a5836..0a2f84f935 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml @@ -11,7 +11,7 @@ <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post"> <h4>Create a new account.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResetPassword.cshtml b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResetPassword.cshtml index 3202efb8ec..953261844b 100644 --- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResetPassword.cshtml +++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResetPassword.cshtml @@ -10,7 +10,7 @@ <div class="row"> <div class="col-md-4"> <form method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Input.Code" type="hidden" /> <div class="form-group"> <label asp-for="Input.Email"></label> diff --git a/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Manage/Index.cshtml b/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Manage/Index.cshtml index 6b2de21b43..23593ab99a 100644 --- a/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Manage/Index.cshtml +++ b/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Manage/Index.cshtml @@ -9,7 +9,7 @@ <div class="row"> <div class="col-md-6"> <form method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Username"></label> <input asp-for="Username" class="form-control" disabled /> diff --git a/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Register.cshtml b/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Register.cshtml index 259373e966..42297ce4d6 100644 --- a/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Register.cshtml +++ b/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Register.cshtml @@ -11,7 +11,7 @@ <form asp-route-returnUrl="@Model.ReturnUrl" method="post"> <h4>Create a new account.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Account/ExternalLoginConfirmation.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Account/ExternalLoginConfirmation.cshtml index 0bd3f75c1a..c9151a4239 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Account/ExternalLoginConfirmation.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Account/ExternalLoginConfirmation.cshtml @@ -9,7 +9,7 @@ <form asp-controller="Account" asp-action="ExternalLoginConfirmation" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form"> <h4>Association Form</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <p class="text-info"> You've successfully authenticated with <strong>@ViewData["ProviderDisplayName"]</strong>. diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Account/ForgotPassword.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Account/ForgotPassword.cshtml index 5603e2a08c..74f769a750 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Account/ForgotPassword.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Account/ForgotPassword.cshtml @@ -11,7 +11,7 @@ @*<form asp-controller="Account" asp-action="ForgotPassword" method="post" class="form-horizontal" role="form"> <h4>Enter your email.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Email" class="col-md-2 control-label"></label> <div class="col-md-10"> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Account/Login.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Account/Login.cshtml index b8803908e1..92e5a4a89a 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Account/Login.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Account/Login.cshtml @@ -15,7 +15,7 @@ <form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form"> <h4>Use a local account to log in.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Email" class="col-md-2 control-label"></label> <div class="col-md-10"> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Account/Register.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Account/Register.cshtml index 44e11d6955..f2d23a465c 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Account/Register.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Account/Register.cshtml @@ -8,7 +8,7 @@ <form asp-controller="Account" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form"> <h4>Create a new account.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Email" class="col-md-2 control-label"></label> <div class="col-md-10"> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Account/ResetPassword.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Account/ResetPassword.cshtml index 343fcd8d33..c815424623 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Account/ResetPassword.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Account/ResetPassword.cshtml @@ -8,7 +8,7 @@ <form asp-controller="Account" asp-action="ResetPassword" method="post" class="form-horizontal" role="form"> <h4>Reset your password.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Code" type="hidden" /> <div class="form-group"> <label asp-for="Email" class="col-md-2 control-label"></label> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Account/UseRecoveryCode.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Account/UseRecoveryCode.cshtml index 7c74d72a23..dd52938373 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Account/UseRecoveryCode.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Account/UseRecoveryCode.cshtml @@ -6,7 +6,7 @@ <h1>@ViewData["Title"].</h1> <form asp-controller="Account" asp-action="UseRecoveryCode" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <h4>@ViewData["Status"]</h4> <hr /> <div class="form-group"> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Account/VerifyAuthenticatorCode.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Account/VerifyAuthenticatorCode.cshtml index f675dd1535..04ead74dbb 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Account/VerifyAuthenticatorCode.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Account/VerifyAuthenticatorCode.cshtml @@ -6,7 +6,7 @@ <h1>@ViewData["Title"].</h1> <form asp-controller="Account" asp-action="VerifyAuthenticatorCode" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="RememberMe" type="hidden" /> <h4>@ViewData["Status"]</h4> <hr /> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Account/VerifyCode.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Account/VerifyCode.cshtml index 330a659ca7..4f15eda730 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Account/VerifyCode.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Account/VerifyCode.cshtml @@ -6,7 +6,7 @@ <h1>@ViewData["Title"].</h1> <form asp-controller="Account" asp-action="VerifyCode" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Provider" type="hidden" /> <input asp-for="RememberMe" type="hidden" /> <h4>@ViewData["Status"]</h4> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Manage/AddPhoneNumber.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Manage/AddPhoneNumber.cshtml index 40885f3832..679dcb0a05 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Manage/AddPhoneNumber.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Manage/AddPhoneNumber.cshtml @@ -7,7 +7,7 @@ <form asp-controller="Manage" asp-action="AddPhoneNumber" method="post" class="form-horizontal" role="form"> <h4>Add a phone number.</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="PhoneNumber" class="col-md-2 control-label"></label> <div class="col-md-10"> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Manage/ChangePassword.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Manage/ChangePassword.cshtml index a8d076821b..4d3b38c13c 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Manage/ChangePassword.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Manage/ChangePassword.cshtml @@ -8,7 +8,7 @@ <form asp-controller="Manage" asp-action="ChangePassword" method="post" class="form-horizontal" role="form"> <h4>Change Password Form</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="OldPassword" class="col-md-2 control-label"></label> <div class="col-md-10"> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Manage/SetPassword.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Manage/SetPassword.cshtml index d1a1b77a44..ccd5781882 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Manage/SetPassword.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Manage/SetPassword.cshtml @@ -11,7 +11,7 @@ <form asp-controller="Manage" asp-action="SetPassword" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form"> <h4>Set your password</h4> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="NewPassword" class="col-md-2 control-label"></label> <div class="col-md-10"> diff --git a/src/Identity/samples/IdentitySample.Mvc/Views/Manage/VerifyPhoneNumber.cshtml b/src/Identity/samples/IdentitySample.Mvc/Views/Manage/VerifyPhoneNumber.cshtml index 076c20562c..6c4718a6f7 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Views/Manage/VerifyPhoneNumber.cshtml +++ b/src/Identity/samples/IdentitySample.Mvc/Views/Manage/VerifyPhoneNumber.cshtml @@ -10,7 +10,7 @@ <h4>Add a phone number.</h4> <h5>@ViewData["Status"]</h5> <hr /> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Code" class="col-md-2 control-label"></label> <div class="col-md-10"> diff --git a/src/Identity/testassets/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml b/src/Identity/testassets/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml index 91d36c62b0..0bcefd7555 100644 --- a/src/Identity/testassets/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml +++ b/src/Identity/testassets/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml @@ -10,7 +10,7 @@ <div class="col-md-4"> <section> <form id="external-login" method="post"> - <div asp-validation-summary="All" class="text-danger"></div> + <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Login"></label> <input asp-for="Input.Login" class="form-control" /> diff --git a/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs b/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs index 02e5c7a0d1..58409f11e5 100644 --- a/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs +++ b/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs @@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure return InvokeCoreAwaited(context, policyTask); } - corsPolicy = policyTask.GetAwaiter().GetResult(); + corsPolicy = policyTask.Result; } return EvaluateAndApplyPolicy(context, corsPolicy); diff --git a/src/Middleware/HealthChecks.EntityFrameworkCore/src/DbContextHealthCheck.cs b/src/Middleware/HealthChecks.EntityFrameworkCore/src/DbContextHealthCheck.cs index 7fa998f296..6481bd29d3 100644 --- a/src/Middleware/HealthChecks.EntityFrameworkCore/src/DbContextHealthCheck.cs +++ b/src/Middleware/HealthChecks.EntityFrameworkCore/src/DbContextHealthCheck.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -49,8 +49,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { return HealthCheckResult.Healthy(); } - - return HealthCheckResult.Unhealthy(); + + return new HealthCheckResult(context.Registration.FailureStatus); } } } diff --git a/src/Middleware/HealthChecks.EntityFrameworkCore/test/DbContextHealthCheckTest.cs b/src/Middleware/HealthChecks.EntityFrameworkCore/test/DbContextHealthCheckTest.cs index fe5383c6f5..df22c3361f 100644 --- a/src/Middleware/HealthChecks.EntityFrameworkCore/test/DbContextHealthCheckTest.cs +++ b/src/Middleware/HealthChecks.EntityFrameworkCore/test/DbContextHealthCheckTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -61,7 +61,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks } [Fact] - public async Task CheckAsync_CustomTest_Degraded() + public async Task CheckAsync_CustomTestWithDegradedFailureStatusSpecified_Degraded() { // Arrange var services = CreateServices(async (c, ct) => @@ -78,12 +78,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); // Assert - Assert.Equal(HealthStatus.Unhealthy, result.Status); + Assert.Equal(HealthStatus.Degraded, result.Status); } } [Fact] - public async Task CheckAsync_CustomTest_Unhealthy() + public async Task CheckAsync_CustomTestWithUnhealthyFailureStatusSpecified_Unhealthy() { // Arrange var services = CreateServices(async (c, ct) => @@ -104,12 +104,34 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks } } + [Fact] + public async Task CheckAsync_CustomTestWithNoFailureStatusSpecified_Unhealthy() + { + // Arrange + var services = CreateServices(async (c, ct) => + { + return 0 < await c.Blogs.CountAsync(); + }, failureStatus: null); + + using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope()) + { + var registration = Assert.Single(services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations); + var check = ActivatorUtilities.CreateInstance<DbContextHealthCheck<TestDbContext>>(scope.ServiceProvider); + + // Act + var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); + + // Assert + Assert.Equal(HealthStatus.Unhealthy, result.Status); + } + } + // used to ensure each test uses a unique in-memory database private static int _testDbCounter; private static IServiceProvider CreateServices( Func<TestDbContext, CancellationToken, Task<bool>> testQuery = null, - HealthStatus failureStatus = HealthStatus.Unhealthy) + HealthStatus? failureStatus = HealthStatus.Unhealthy) { var serviceCollection = new ServiceCollection(); serviceCollection.AddDbContext<TestDbContext>(o => o.UseInMemoryDatabase("Test" + Interlocked.Increment(ref _testDbCounter))); diff --git a/src/Middleware/Localization/Localization.slnf b/src/Middleware/Localization/Localization.slnf new file mode 100644 index 0000000000..e4494b3619 --- /dev/null +++ b/src/Middleware/Localization/Localization.slnf @@ -0,0 +1,16 @@ +{ + "solution": { + "path": "..\\Middleware.sln", + "projects": [ + "Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", + "Localization.Routing\\test\\Microsoft.AspNetCore.Localization.Routing.Tests.csproj", + "Localization\\sample\\LocalizationSample.csproj", + "Localization\\src\\Microsoft.AspNetCore.Localization.csproj", + "Localization\\test\\FunctionalTests\\Microsoft.AspNetCore.Localization.FunctionalTests.csproj", + "Localization\\test\\UnitTests\\Microsoft.AspNetCore.Localization.Tests.csproj", + "Localization\\testassets\\LocalizationWebsite\\LocalizationWebsite.csproj", + "Localization\\testassets\\ResourcesClassLibraryNoAttribute\\ResourcesClassLibraryNoAttribute.csproj", + "Localization\\testassets\\ResourcesClassLibraryWithAttribute\\ResourcesClassLibraryWithAttribute.csproj" + ] + } +}
\ No newline at end of file diff --git a/src/Middleware/Localization/ref/Microsoft.AspNetCore.Localization.netcoreapp.cs b/src/Middleware/Localization/ref/Microsoft.AspNetCore.Localization.netcoreapp.cs index 599db4aca6..558921ba6b 100644 --- a/src/Middleware/Localization/ref/Microsoft.AspNetCore.Localization.netcoreapp.cs +++ b/src/Middleware/Localization/ref/Microsoft.AspNetCore.Localization.netcoreapp.cs @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Builder public partial class RequestLocalizationOptions { public RequestLocalizationOptions() { } + public bool ApplyCurrentCultureToResponseHeaders { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Localization.RequestCulture DefaultRequestCulture { get { throw null; } set { } } public bool FallBackToParentCultures { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public bool FallBackToParentUICultures { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } diff --git a/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs b/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs index c6ec3e1c06..07277ecdd7 100644 --- a/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs +++ b/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; @@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Localization { @@ -146,6 +147,11 @@ namespace Microsoft.AspNetCore.Localization SetCurrentThreadCulture(requestCulture); + if (_options.ApplyCurrentCultureToResponseHeaders) + { + context.Response.Headers.Add(HeaderNames.ContentLanguage, requestCulture.UICulture.Name); + } + await _next(context); } diff --git a/src/Middleware/Localization/src/RequestLocalizationOptions.cs b/src/Middleware/Localization/src/RequestLocalizationOptions.cs index 16776364f0..95ca74fe32 100644 --- a/src/Middleware/Localization/src/RequestLocalizationOptions.cs +++ b/src/Middleware/Localization/src/RequestLocalizationOptions.cs @@ -85,6 +85,11 @@ namespace Microsoft.AspNetCore.Builder public bool FallBackToParentUICultures { get; set; } = true; /// <summary> + /// Gets or sets a value that determines if <see cref="CultureInfo.CurrentUICulture" /> is applied to the response <c>Content-Language</c> header. + /// </summary> + public bool ApplyCurrentCultureToResponseHeaders { get; set; } + + /// <summary> /// The cultures supported by the application. The <see cref="RequestLocalizationMiddleware"/> will only set /// the current request culture to an entry in this list. /// Defaults to <see cref="CultureInfo.CurrentCulture"/>. diff --git a/src/Middleware/Localization/test/FunctionalTests/LocalizationTest.cs b/src/Middleware/Localization/test/FunctionalTests/LocalizationTest.cs index 5f09115be2..15b00dfacf 100644 --- a/src/Middleware/Localization/test/FunctionalTests/LocalizationTest.cs +++ b/src/Middleware/Localization/test/FunctionalTests/LocalizationTest.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Net; @@ -15,6 +15,15 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests public class LocalizationTest { [Fact] + public Task Localization_ContentLanguageHeader() + { + return RunTest( + typeof(StartupContentLanguageHeader), + "ar-YE", + "True ar-YE"); + } + + [Fact] public Task Localization_CustomCulture() { return RunTest( diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupContentLanguageHeader.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupContentLanguageHeader.cs new file mode 100644 index 0000000000..849743184c --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupContentLanguageHeader.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; + +namespace LocalizationWebsite +{ + public class StartupContentLanguageHeader + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(); + } + + public void Configure( + IApplicationBuilder app) + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List<CultureInfo>() + { + new CultureInfo("ar-YE") + }, + SupportedUICultures = new List<CultureInfo>() + { + new CultureInfo("ar-YE") + }, + ApplyCurrentCultureToResponseHeaders = true + }); + + app.Run(async (context) => + { + var hasContentLanguageHeader = context.Response.Headers.ContainsKey(HeaderNames.ContentLanguage); + var contentLanguage = context.Response.Headers[HeaderNames.ContentLanguage].ToString(); + + await context.Response.WriteAsync(hasContentLanguageHeader.ToString()); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(contentLanguage); + }); + } + } +}
\ No newline at end of file diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInClassLibrary.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInClassLibrary.cs index 092d09a37b..6cab0c27c6 100644 --- a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInClassLibrary.cs +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInClassLibrary.cs @@ -39,7 +39,7 @@ namespace LocalizationWebsite }); var noAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(ResourcesClassLibraryNoAttribute.Model)); - var withAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(ResourcesClassLibraryWithAttribute.Model)); + var withAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(Alternate.Namespace.Model)); var noAttributeAssembly = typeof(ResourcesClassLibraryNoAttribute.Model).GetTypeInfo().Assembly; var noAttributeName = new AssemblyName(noAttributeAssembly.FullName).Name; @@ -47,10 +47,10 @@ namespace LocalizationWebsite nameof(ResourcesClassLibraryNoAttribute.Model), noAttributeName); - var withAttributeAssembly = typeof(ResourcesClassLibraryWithAttribute.Model).GetTypeInfo().Assembly; + var withAttributeAssembly = typeof(Alternate.Namespace.Model).GetTypeInfo().Assembly; var withAttributeName = new AssemblyName(withAttributeAssembly.FullName).Name; var withAttributeNameStringLocalizer = stringLocalizerFactory.Create( - nameof(ResourcesClassLibraryWithAttribute.Model), + nameof(Alternate.Namespace.Model), withAttributeName); app.Run(async (context) => diff --git a/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/Model.cs b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/Model.cs index c6ca99afa8..b1ff90f523 100644 --- a/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/Model.cs +++ b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/Model.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace ResourcesClassLibraryWithAttribute +namespace Alternate.Namespace { public class Model { diff --git a/src/Middleware/SpaServices.Extensions/ref/Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp.cs b/src/Middleware/SpaServices.Extensions/ref/Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp.cs index 97f23b740d..6691b79a64 100644 --- a/src/Middleware/SpaServices.Extensions/ref/Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp.cs +++ b/src/Middleware/SpaServices.Extensions/ref/Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp.cs @@ -41,6 +41,7 @@ namespace Microsoft.AspNetCore.SpaServices public SpaOptions() { } public Microsoft.AspNetCore.Http.PathString DefaultPage { get { throw null; } set { } } public Microsoft.AspNetCore.Builder.StaticFileOptions DefaultPageStaticFileOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public string PackageManagerCommand { get { throw null; } set { } } public string SourcePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.TimeSpan StartupTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs index fa3e9cc449..24f0ba430f 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli { private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine - private readonly string _npmScriptName; + private readonly string _scriptName; /// <summary> /// Constructs an instance of <see cref="AngularCliBuilder"/>. @@ -35,12 +35,13 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli throw new ArgumentException("Cannot be null or empty.", nameof(npmScript)); } - _npmScriptName = npmScript; + _scriptName = npmScript; } /// <inheritdoc /> public async Task Build(ISpaBuilder spaBuilder) { + var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand; var sourcePath = spaBuilder.Options.SourcePath; if (string.IsNullOrEmpty(sourcePath)) { @@ -50,32 +51,33 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli var logger = LoggerFinder.GetOrCreateLogger( spaBuilder.ApplicationBuilder, nameof(AngularCliBuilder)); - var npmScriptRunner = new NpmScriptRunner( + var scriptRunner = new NodeScriptRunner( sourcePath, - _npmScriptName, + _scriptName, "--watch", - null); - npmScriptRunner.AttachToLogger(logger); + null, + pkgManagerCommand); + scriptRunner.AttachToLogger(logger); - using (var stdOutReader = new EventedStreamStringReader(npmScriptRunner.StdOut)) - using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) + using (var stdOutReader = new EventedStreamStringReader(scriptRunner.StdOut)) + using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) { try { - await npmScriptRunner.StdOut.WaitForMatch( + await scriptRunner.StdOut.WaitForMatch( new Regex("Date", RegexOptions.None, RegexMatchTimeout)); } catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The NPM script '{_npmScriptName}' exited without indicating success.\n" + + $"The {pkgManagerCommand} script '{_scriptName}' exited without indicating success.\n" + $"Output was: {stdOutReader.ReadAsString()}\n" + $"Error output was: {stdErrReader.ReadAsString()}", ex); } catch (OperationCanceledException ex) { throw new InvalidOperationException( - $"The NPM script '{_npmScriptName}' timed out without indicating success. " + + $"The {pkgManagerCommand} script '{_scriptName}' timed out without indicating success. " + $"Output was: {stdOutReader.ReadAsString()}\n" + $"Error output was: {stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs index 9090f7738b..c4e109b8f7 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs @@ -23,23 +23,24 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli public static void Attach( ISpaBuilder spaBuilder, - string npmScriptName) + string scriptName) { + var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand; var sourcePath = spaBuilder.Options.SourcePath; if (string.IsNullOrEmpty(sourcePath)) { throw new ArgumentException("Cannot be null or empty", nameof(sourcePath)); } - if (string.IsNullOrEmpty(npmScriptName)) + if (string.IsNullOrEmpty(scriptName)) { - throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName)); + throw new ArgumentException("Cannot be null or empty", nameof(scriptName)); } // Start Angular CLI and attach to middleware pipeline var appBuilder = spaBuilder.ApplicationBuilder; var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); - var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, logger); + var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerCommand, logger); // Everything we proxy is hardcoded to target http://localhost because: // - the requests are always from the local machine (we're not accepting remote @@ -62,27 +63,27 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli } private static async Task<AngularCliServerInfo> StartAngularCliServerAsync( - string sourcePath, string npmScriptName, ILogger logger) + string sourcePath, string scriptName, string pkgManagerCommand, ILogger logger) { var portNumber = TcpPortFinder.FindAvailablePort(); logger.LogInformation($"Starting @angular/cli on port {portNumber}..."); - var npmScriptRunner = new NpmScriptRunner( - sourcePath, npmScriptName, $"--port {portNumber}", null); - npmScriptRunner.AttachToLogger(logger); + var scriptRunner = new NodeScriptRunner( + sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerCommand); + scriptRunner.AttachToLogger(logger); Match openBrowserLine; - using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) + using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) { try { - openBrowserLine = await npmScriptRunner.StdOut.WaitForMatch( + openBrowserLine = await scriptRunner.StdOut.WaitForMatch( new Regex("open your browser on (http\\S+)", RegexOptions.None, RegexMatchTimeout)); } catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The NPM script '{npmScriptName}' exited without indicating that the " + + $"The {pkgManagerCommand} script '{scriptName}' exited without indicating that the " + $"Angular CLI was listening for requests. The error output was: " + $"{stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs index 28e63c8e35..8f8176447b 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Builder; diff --git a/src/Middleware/SpaServices.Extensions/src/Npm/NpmScriptRunner.cs b/src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs index 378ec5f9fa..f08abeb19c 100644 --- a/src/Middleware/SpaServices.Extensions/src/Npm/NpmScriptRunner.cs +++ b/src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.Extensions.Logging; @@ -16,14 +16,14 @@ namespace Microsoft.AspNetCore.NodeServices.Npm /// Executes the <c>script</c> entries defined in a <c>package.json</c> file, /// capturing any output written to stdio. /// </summary> - internal class NpmScriptRunner + internal class NodeScriptRunner { public EventedStreamReader StdOut { get; } public EventedStreamReader StdErr { get; } private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1)); - public NpmScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars) + public NodeScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars, string pkgManagerCommand) { if (string.IsNullOrEmpty(workingDirectory)) { @@ -35,18 +35,23 @@ namespace Microsoft.AspNetCore.NodeServices.Npm throw new ArgumentException("Cannot be null or empty.", nameof(scriptName)); } - var npmExe = "npm"; + if (string.IsNullOrEmpty(pkgManagerCommand)) + { + throw new ArgumentException("Cannot be null or empty.", nameof(pkgManagerCommand)); + } + + var exeToRun = pkgManagerCommand; var completeArguments = $"run {scriptName} -- {arguments ?? string.Empty}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // On Windows, the NPM executable is a .cmd file, so it can't be executed + // On Windows, the node executable is a .cmd file, so it can't be executed // directly (except with UseShellExecute=true, but that's no good, because // it prevents capturing stdio). So we need to invoke it via "cmd /c". - npmExe = "cmd"; - completeArguments = $"/c npm {completeArguments}"; + exeToRun = "cmd"; + completeArguments = $"/c {pkgManagerCommand} {completeArguments}"; } - var processStartInfo = new ProcessStartInfo(npmExe) + var processStartInfo = new ProcessStartInfo(exeToRun) { Arguments = completeArguments, UseShellExecute = false, @@ -64,19 +69,19 @@ namespace Microsoft.AspNetCore.NodeServices.Npm } } - var process = LaunchNodeProcess(processStartInfo); + var process = LaunchNodeProcess(processStartInfo, pkgManagerCommand); StdOut = new EventedStreamReader(process.StandardOutput); StdErr = new EventedStreamReader(process.StandardError); } public void AttachToLogger(ILogger logger) { - // When the NPM task emits complete lines, pass them through to the real logger + // When the node task emits complete lines, pass them through to the real logger StdOut.OnReceivedLine += line => { if (!string.IsNullOrWhiteSpace(line)) { - // NPM tasks commonly emit ANSI colors, but it wouldn't make sense to forward + // Node tasks commonly emit ANSI colors, but it wouldn't make sense to forward // those to loggers (because a logger isn't necessarily any kind of terminal) logger.LogInformation(StripAnsiColors(line)); } @@ -106,7 +111,7 @@ namespace Microsoft.AspNetCore.NodeServices.Npm private static string StripAnsiColors(string line) => AnsiColorRegex.Replace(line, string.Empty); - private static Process LaunchNodeProcess(ProcessStartInfo startInfo) + private static Process LaunchNodeProcess(ProcessStartInfo startInfo, string commandName) { try { @@ -119,8 +124,8 @@ namespace Microsoft.AspNetCore.NodeServices.Npm } catch (Exception ex) { - var message = $"Failed to start 'npm'. To resolve this:.\n\n" - + "[1] Ensure that 'npm' is installed and can be found in one of the PATH directories.\n" + var message = $"Failed to start '{commandName}'. To resolve this:.\n\n" + + $"[1] Ensure that '{commandName}' is installed and can be found in one of the PATH directories.\n" + $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n" + " Make sure the executable is in one of those directories, or update your PATH.\n\n" + "[2] See the InnerException for further details of the cause."; diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs index 78a7b4f03f..6566fef706 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs @@ -22,23 +22,24 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer public static void Attach( ISpaBuilder spaBuilder, - string npmScriptName) + string scriptName) { + var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand; var sourcePath = spaBuilder.Options.SourcePath; if (string.IsNullOrEmpty(sourcePath)) { throw new ArgumentException("Cannot be null or empty", nameof(sourcePath)); } - if (string.IsNullOrEmpty(npmScriptName)) + if (string.IsNullOrEmpty(scriptName)) { - throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName)); + throw new ArgumentException("Cannot be null or empty", nameof(scriptName)); } // Start create-react-app and attach to middleware pipeline var appBuilder = spaBuilder.ApplicationBuilder; var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); - var portTask = StartCreateReactAppServerAsync(sourcePath, npmScriptName, logger); + var portTask = StartCreateReactAppServerAsync(sourcePath, scriptName, pkgManagerCommand, logger); // Everything we proxy is hardcoded to target http://localhost because: // - the requests are always from the local machine (we're not accepting remote @@ -61,7 +62,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer } private static async Task<int> StartCreateReactAppServerAsync( - string sourcePath, string npmScriptName, ILogger logger) + string sourcePath, string scriptName, string pkgManagerCommand, ILogger logger) { var portNumber = TcpPortFinder.FindAvailablePort(); logger.LogInformation($"Starting create-react-app server on port {portNumber}..."); @@ -71,11 +72,11 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer { "PORT", portNumber.ToString() }, { "BROWSER", "none" }, // We don't want create-react-app to open its own extra browser window pointing to the internal dev server port }; - var npmScriptRunner = new NpmScriptRunner( - sourcePath, npmScriptName, null, envVars); - npmScriptRunner.AttachToLogger(logger); + var scriptRunner = new NodeScriptRunner( + sourcePath, scriptName, null, envVars, pkgManagerCommand); + scriptRunner.AttachToLogger(logger); - using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) + using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) { try { @@ -83,13 +84,13 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer // it doesn't do so until it's finished compiling, and even then only if there were // no compiler warnings. So instead of waiting for that, consider it ready as soon // as it starts listening for requests. - await npmScriptRunner.StdOut.WaitForMatch( + await scriptRunner.StdOut.WaitForMatch( new Regex("Starting the development server", RegexOptions.None, RegexMatchTimeout)); } catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The NPM script '{npmScriptName}' exited without indicating that the " + + $"The {pkgManagerCommand} script '{scriptName}' exited without indicating that the " + $"create-react-app server was listening for requests. The error output was: " + $"{stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs index f58a6d1a9d..346e839046 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Builder; diff --git a/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs b/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs index b2823396dc..59ccc1eda4 100644 --- a/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs +++ b/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs @@ -1,11 +1,10 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.FileProviders; -using System; namespace Microsoft.AspNetCore.SpaServices { @@ -15,6 +14,7 @@ namespace Microsoft.AspNetCore.SpaServices public class SpaOptions { private PathString _defaultPage = "/index.html"; + private string _packageManagerCommand = "npm"; /// <summary> /// Constructs a new instance of <see cref="SpaOptions"/>. @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices internal SpaOptions(SpaOptions copyFromOptions) { _defaultPage = copyFromOptions.DefaultPage; + _packageManagerCommand = copyFromOptions.PackageManagerCommand; DefaultPageStaticFileOptions = copyFromOptions.DefaultPageStaticFileOptions; SourcePath = copyFromOptions.SourcePath; } @@ -70,6 +71,26 @@ namespace Microsoft.AspNetCore.SpaServices public string SourcePath { get; set; } /// <summary> + /// Gets or sets the name of the package manager executible, (e.g npm, + /// yarn) to run the SPA. + /// + /// The default value is 'npm'. + /// </summary> + public string PackageManagerCommand + { + get => _packageManagerCommand; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException($"The value for {nameof(PackageManagerCommand)} cannot be null or empty."); + } + + _packageManagerCommand = value; + } + } + + /// <summary> /// Gets or sets the maximum duration that a request will wait for the SPA /// to become ready to serve to the client. /// </summary> diff --git a/src/Middleware/SpaServices.Extensions/src/Util/EventedStreamReader.cs b/src/Middleware/SpaServices.Extensions/src/Util/EventedStreamReader.cs index 95e018a590..aafd630853 100644 --- a/src/Middleware/SpaServices.Extensions/src/Util/EventedStreamReader.cs +++ b/src/Middleware/SpaServices.Extensions/src/Util/EventedStreamReader.cs @@ -83,23 +83,35 @@ namespace Microsoft.AspNetCore.NodeServices.Util var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length); if (chunkLength == 0) { + if (_linesBuffer.Length > 0) + { + OnCompleteLine(_linesBuffer.ToString()); + _linesBuffer.Clear(); + } + OnClosed(); break; } OnChunk(new ArraySegment<char>(buf, 0, chunkLength)); - var lineBreakPos = Array.IndexOf(buf, '\n', 0, chunkLength); - if (lineBreakPos < 0) - { - _linesBuffer.Append(buf, 0, chunkLength); - } - else + int lineBreakPos = -1; + int startPos = 0; + + // get all the newlines + while ((lineBreakPos = Array.IndexOf(buf, '\n', startPos, chunkLength - startPos)) >= 0 && startPos < chunkLength) { - _linesBuffer.Append(buf, 0, lineBreakPos + 1); + var length = (lineBreakPos + 1) - startPos; + _linesBuffer.Append(buf, startPos, length); OnCompleteLine(_linesBuffer.ToString()); _linesBuffer.Clear(); - _linesBuffer.Append(buf, lineBreakPos + 1, chunkLength - (lineBreakPos + 1)); + startPos = lineBreakPos + 1; + } + + // get the rest + if (lineBreakPos < 0 && startPos < chunkLength) + { + _linesBuffer.Append(buf, startPos, chunkLength); } } } diff --git a/src/Middleware/StaticFiles/ref/Microsoft.AspNetCore.StaticFiles.netcoreapp.cs b/src/Middleware/StaticFiles/ref/Microsoft.AspNetCore.StaticFiles.netcoreapp.cs index 424801bb14..155d816cb8 100644 --- a/src/Middleware/StaticFiles/ref/Microsoft.AspNetCore.StaticFiles.netcoreapp.cs +++ b/src/Middleware/StaticFiles/ref/Microsoft.AspNetCore.StaticFiles.netcoreapp.cs @@ -120,12 +120,14 @@ namespace Microsoft.AspNetCore.StaticFiles.Infrastructure { public SharedOptions() { } public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool RedirectToAppendTrailingSlash { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Http.PathString RequestPath { get { throw null; } set { } } } public abstract partial class SharedOptionsBase { protected SharedOptionsBase(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions sharedOptions) { } public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { get { throw null; } set { } } + public bool RedirectToAppendTrailingSlash { get { throw null; } set { } } public Microsoft.AspNetCore.Http.PathString RequestPath { get { throw null; } set { } } protected Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions SharedOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } diff --git a/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs b/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs index 3aabe2fe65..b3b4755789 100644 --- a/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs +++ b/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs @@ -80,17 +80,13 @@ namespace Microsoft.AspNetCore.StaticFiles { // If the path matches a directory but does not end in a slash, redirect to add the slash. // This prevents relative links from breaking. - if (!Helpers.PathEndsInSlash(context.Request.Path)) + if (!Helpers.PathEndsInSlash(context.Request.Path) && _options.RedirectToAppendTrailingSlash) { - context.Response.StatusCode = StatusCodes.Status301MovedPermanently; - var request = context.Request; - var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString); - context.Response.Headers[HeaderNames.Location] = redirect; + Helpers.RedirectToPathWithSlash(context); return Task.CompletedTask; } - // Match found, re-write the url. A later middleware will actually serve the file. - context.Request.Path = new PathString(context.Request.Path.Value + defaultFile); + context.Request.Path = new PathString(Helpers.GetPathValueWithSlash(context.Request.Path) + defaultFile); break; } } diff --git a/src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs b/src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs index 2d0a07b509..e689b309e4 100644 --- a/src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs +++ b/src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs @@ -87,12 +87,9 @@ namespace Microsoft.AspNetCore.StaticFiles { // If the path matches a directory but does not end in a slash, redirect to add the slash. // This prevents relative links from breaking. - if (!Helpers.PathEndsInSlash(context.Request.Path)) + if (!Helpers.PathEndsInSlash(context.Request.Path) && _options.RedirectToAppendTrailingSlash) { - context.Response.StatusCode = StatusCodes.Status301MovedPermanently; - var request = context.Request; - var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString); - context.Response.Headers[HeaderNames.Location] = redirect; + Helpers.RedirectToPathWithSlash(context); return Task.CompletedTask; } diff --git a/src/Middleware/StaticFiles/src/Helpers.cs b/src/Middleware/StaticFiles/src/Helpers.cs index a7a49e9070..d9b29c082f 100644 --- a/src/Middleware/StaticFiles/src/Helpers.cs +++ b/src/Middleware/StaticFiles/src/Helpers.cs @@ -2,9 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.FileProviders; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.StaticFiles { @@ -12,7 +15,8 @@ namespace Microsoft.AspNetCore.StaticFiles { internal static IFileProvider ResolveFileProvider(IWebHostEnvironment hostingEnv) { - if (hostingEnv.WebRootFileProvider == null) { + if (hostingEnv.WebRootFileProvider == null) + { throw new InvalidOperationException("Missing FileProvider."); } return hostingEnv.WebRootFileProvider; @@ -28,6 +32,23 @@ namespace Microsoft.AspNetCore.StaticFiles return path.Value.EndsWith("/", StringComparison.Ordinal); } + internal static string GetPathValueWithSlash(PathString path) + { + if (!PathEndsInSlash(path)) + { + return path.Value + "/"; + } + return path.Value; + } + + internal static void RedirectToPathWithSlash(HttpContext context) + { + context.Response.StatusCode = StatusCodes.Status301MovedPermanently; + var request = context.Request; + var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString); + context.Response.Headers[HeaderNames.Location] = redirect; + } + internal static bool TryMatchPath(HttpContext context, PathString matchUrl, bool forDirectory, out PathString subpath) { var path = context.Request.Path; diff --git a/src/Middleware/StaticFiles/src/Infrastructure/SharedOptions.cs b/src/Middleware/StaticFiles/src/Infrastructure/SharedOptions.cs index 1c1cc80ad5..d6f08129a1 100644 --- a/src/Middleware/StaticFiles/src/Infrastructure/SharedOptions.cs +++ b/src/Middleware/StaticFiles/src/Infrastructure/SharedOptions.cs @@ -42,5 +42,10 @@ namespace Microsoft.AspNetCore.StaticFiles.Infrastructure /// The file system used to locate resources /// </summary> public IFileProvider FileProvider { get; set; } + + /// <summary> + /// Indicates whether to redirect to add a trailing slash at the end of path. Relative resource links may require this. + /// </summary> + public bool RedirectToAppendTrailingSlash { get; set; } = true; } } diff --git a/src/Middleware/StaticFiles/src/Infrastructure/SharedOptionsBase.cs b/src/Middleware/StaticFiles/src/Infrastructure/SharedOptionsBase.cs index 16900ec6fb..9e41b96cdc 100644 --- a/src/Middleware/StaticFiles/src/Infrastructure/SharedOptionsBase.cs +++ b/src/Middleware/StaticFiles/src/Infrastructure/SharedOptionsBase.cs @@ -48,5 +48,14 @@ namespace Microsoft.AspNetCore.StaticFiles.Infrastructure get { return SharedOptions.FileProvider; } set { SharedOptions.FileProvider = value; } } + + /// <summary> + /// Indicates whether to redirect to add a trailing slash at the end of path. Relative resource links may require this. + /// </summary> + public bool RedirectToAppendTrailingSlash + { + get { return SharedOptions.RedirectToAppendTrailingSlash; } + set { SharedOptions.RedirectToAppendTrailingSlash = value; } + } } } diff --git a/src/Middleware/StaticFiles/test/FunctionalTests/FallbackStaticFileTest.cs b/src/Middleware/StaticFiles/test/FunctionalTests/FallbackStaticFileTest.cs index 2d12ee7a30..58b80d2952 100644 --- a/src/Middleware/StaticFiles/test/FunctionalTests/FallbackStaticFileTest.cs +++ b/src/Middleware/StaticFiles/test/FunctionalTests/FallbackStaticFileTest.cs @@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.StaticFiles using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) { var environment = server.Services.GetRequiredService<IWebHostEnvironment>(); - using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) + using (var client = new HttpClient { BaseAddress = new Uri(Helpers.GetAddress(server)) }) { var response = await client.GetAsync("hello"); var responseText = await response.Content.ReadAsStringAsync(); @@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.StaticFiles using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) { var environment = server.Services.GetRequiredService<IWebHostEnvironment>(); - using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) + using (var client = new HttpClient { BaseAddress = new Uri(Helpers.GetAddress(server)) }) { var response = await client.GetAsync("hello"); var responseText = await response.Content.ReadAsStringAsync(); diff --git a/src/Middleware/StaticFiles/test/FunctionalTests/Helpers.cs b/src/Middleware/StaticFiles/test/FunctionalTests/Helpers.cs new file mode 100644 index 0000000000..12e634cda1 --- /dev/null +++ b/src/Middleware/StaticFiles/test/FunctionalTests/Helpers.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; + +namespace Microsoft.AspNetCore.StaticFiles +{ + public static class Helpers + { + public static string GetAddress(IWebHost server) + { + return server.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First(); + } + } +} diff --git a/src/Middleware/StaticFiles/test/FunctionalTests/StaticFileMiddlewareTests.cs b/src/Middleware/StaticFiles/test/FunctionalTests/StaticFileMiddlewareTests.cs index f90c79ca12..b4ca20cf0a 100644 --- a/src/Middleware/StaticFiles/test/FunctionalTests/StaticFileMiddlewareTests.cs +++ b/src/Middleware/StaticFiles/test/FunctionalTests/StaticFileMiddlewareTests.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.StaticFiles using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) { - using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) + using (var client = new HttpClient { BaseAddress = new Uri(Helpers.GetAddress(server)) }) { var response = await client.GetAsync("TestDocument.txt"); @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.StaticFiles using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) { - using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) + using (var client = new HttpClient { BaseAddress = new Uri(Helpers.GetAddress(server)) }) { var response = await client.GetAsync("TestDocument.txt"); @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.StaticFiles using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) { - using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) + using (var client = new HttpClient { BaseAddress = new Uri(Helpers.GetAddress(server)) }) { var last = File.GetLastWriteTimeUtc(Path.Combine(AppContext.BaseDirectory, "TestDocument.txt")); var response = await client.GetAsync("TestDocument.txt"); @@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.StaticFiles { var hostingEnvironment = server.Services.GetService<IWebHostEnvironment>(); - using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) + using (var client = new HttpClient { BaseAddress = new Uri(Helpers.GetAddress(server)) }) { var fileInfo = hostingEnvironment.WebRootFileProvider.GetFileInfo(Path.GetFileName(requestUrl)); var response = await client.GetAsync(requestUrl); @@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.StaticFiles { var hostingEnvironment = server.Services.GetService<IWebHostEnvironment>(); - using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) + using (var client = new HttpClient { BaseAddress = new Uri(Helpers.GetAddress(server)) }) { var fileInfo = hostingEnvironment.WebRootFileProvider.GetFileInfo(Path.GetFileName(requestUrl)); var request = new HttpRequestMessage(HttpMethod.Head, requestUrl); @@ -261,7 +261,7 @@ namespace Microsoft.AspNetCore.StaticFiles using (var server = builder.Start(TestUrlHelper.GetTestUrl(serverType))) { // We don't use HttpClient here because it's disconnect behavior varies across platforms. - var socket = SendSocketRequestAsync(server.GetAddress(), "/TestDocument1MB.txt"); + var socket = SendSocketRequestAsync(Helpers.GetAddress(server), "/TestDocument1MB.txt"); await requestReceived.Task.TimeoutAfter(interval); socket.LingerState = new LingerOption(true, 0); diff --git a/src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs b/src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs index ab456fd218..08e86714a4 100644 --- a/src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs +++ b/src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs @@ -38,9 +38,14 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("/subdir", @".", "/subdir/missing.dir")] [InlineData("/subdir", @".", "/subdir/missing.dir/")] [InlineData("", @"./", "/missing.dir")] - public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".", "/missing.dir", false)] + [InlineData("", @".", "/missing.dir/", false)] + [InlineData("/subdir", @".", "/subdir/missing.dir", false)] + [InlineData("/subdir", @".", "/subdir/missing.dir/", false)] + [InlineData("", @"./", "/missing.dir", false)] + public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); + await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash); } [ConditionalTheory] @@ -48,12 +53,14 @@ namespace Microsoft.AspNetCore.StaticFiles [OSSkipCondition(OperatingSystems.MacOSX)] [InlineData("", @".\", "/missing.dir")] [InlineData("", @".\", "/Missing.dir")] - public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".\", "/missing.dir", false)] + [InlineData("", @".\", "/Missing.dir", false)] + public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); + await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash); } - private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) + private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) { @@ -62,7 +69,8 @@ namespace Microsoft.AspNetCore.StaticFiles app.UseDefaultFiles(new DefaultFilesOptions { RequestPath = new PathString(baseUrl), - FileProvider = fileProvider + FileProvider = fileProvider, + RedirectToAppendTrailingSlash = appendTrailingSlash }); app.Run(context => context.Response.WriteAsync(context.Request.Path.Value)); }); @@ -102,7 +110,7 @@ namespace Microsoft.AspNetCore.StaticFiles FileProvider = fileProvider }); - app.UseEndpoints(endpoints => {}); + app.UseEndpoints(endpoints => { }); }, services => { services.AddDirectoryBrowser(); services.AddRouting(); }); @@ -118,9 +126,19 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("", @"./SubFolder", "/")] [InlineData("", @"./SubFolder", "/你好/")] [InlineData("", @"./SubFolder", "/你好/世界/")] - public async Task FoundDirectoryWithDefaultFile_PathModified_All(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".", "/SubFolder/", false)] + [InlineData("", @"./", "/SubFolder/", false)] + [InlineData("", @"./SubFolder", "/", false)] + [InlineData("", @"./SubFolder", "/你好/", false)] + [InlineData("", @"./SubFolder", "/你好/世界/", false)] + [InlineData("", @".", "/SubFolder", false)] + [InlineData("", @"./", "/SubFolder", false)] + [InlineData("", @"./SubFolder", "", false)] + [InlineData("", @"./SubFolder", "/你好", false)] + [InlineData("", @"./SubFolder", "/你好/世界", false)] + public async Task FoundDirectoryWithDefaultFile_PathModified_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl); + await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl, appendTrailingSlash); } [ConditionalTheory] @@ -130,12 +148,20 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("", @".\subFolder", "/")] [InlineData("", @".\SubFolder", "/你好/")] [InlineData("", @".\SubFolder", "/你好/世界/")] - public async Task FoundDirectoryWithDefaultFile_PathModified_Windows(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".\", "/SubFolder/", false)] + [InlineData("", @".\subFolder", "/", false)] + [InlineData("", @".\SubFolder", "/你好/", false)] + [InlineData("", @".\SubFolder", "/你好/世界/", false)] + [InlineData("", @".\", "/SubFolder", false)] + [InlineData("", @".\subFolder", "", false)] + [InlineData("", @".\SubFolder", "/你好", false)] + [InlineData("", @".\SubFolder", "/你好/世界", false)] + public async Task FoundDirectoryWithDefaultFile_PathModified_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl); + await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl, appendTrailingSlash); } - private async Task FoundDirectoryWithDefaultFile_PathModified(string baseUrl, string baseDir, string requestUrl) + private async Task FoundDirectoryWithDefaultFile_PathModified(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) { @@ -144,14 +170,17 @@ namespace Microsoft.AspNetCore.StaticFiles app.UseDefaultFiles(new DefaultFilesOptions { RequestPath = new PathString(baseUrl), - FileProvider = fileProvider + FileProvider = fileProvider, + RedirectToAppendTrailingSlash = appendTrailingSlash }); app.Run(context => context.Response.WriteAsync(context.Request.Path.Value)); }); var response = await server.CreateClient().GetAsync(requestUrl); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(requestUrl + "default.html", await response.Content.ReadAsStringAsync()); // Should be modified + var requestUrlWithSlash = requestUrl.EndsWith("/") ? requestUrl : requestUrl + "/"; + Assert.Equal(requestUrlWithSlash + "default.html", await response.Content.ReadAsStringAsync()); // Should be modified and be valid path to file } } @@ -202,9 +231,17 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("/SubFolder", @".", "/somedir/")] [InlineData("", @"./SubFolder", "/")] [InlineData("", @"./SubFolder/", "/")] - public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) + [InlineData("/SubFolder", @"./", "/SubFolder/", false)] + [InlineData("/SubFolder", @".", "/somedir/", false)] + [InlineData("", @"./SubFolder", "/", false)] + [InlineData("", @"./SubFolder/", "/", false)] + [InlineData("/SubFolder", @"./", "/SubFolder", false)] + [InlineData("/SubFolder", @".", "/somedir", false)] + [InlineData("", @"./SubFolder", "", false)] + [InlineData("", @"./SubFolder/", "", false)] + public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); + await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash); } [ConditionalTheory] @@ -213,24 +250,37 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("/SubFolder", @".\", "/SubFolder/")] [InlineData("", @".\SubFolder", "/")] [InlineData("", @".\SubFolder\", "/")] - public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) + [InlineData("/SubFolder", @".\", "/SubFolder/", false)] + [InlineData("", @".\SubFolder", "/", false)] + [InlineData("", @".\SubFolder\", "/", false)] + [InlineData("/SubFolder", @".\", "/SubFolder", false)] + [InlineData("", @".\SubFolder", "", false)] + [InlineData("", @".\SubFolder\", "", false)] + public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); + await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash); } - private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl) + private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) { var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions { RequestPath = new PathString(baseUrl), - FileProvider = fileProvider + FileProvider = fileProvider, + RedirectToAppendTrailingSlash = appendTrailingSlash })); var response = await server.CreateRequest(requestUrl).GetAsync(); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); // Passed through } } + + [Fact] + public void Options_AppendTrailingSlashByDefault() + { + Assert.True(new DefaultFilesOptions().RedirectToAppendTrailingSlash); + } } } diff --git a/src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs b/src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs index 3e909a24ab..5a39ec3ef9 100644 --- a/src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs +++ b/src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs @@ -56,9 +56,14 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("/subdir", @".", "/subdir/missing.dir")] [InlineData("/subdir", @".", "/subdir/missing.dir/")] [InlineData("", @"./", "/missing.dir")] - public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".", "/missing.dir", false)] + [InlineData("", @".", "/missing.dir/", false)] + [InlineData("/subdir", @".", "/subdir/missing.dir", false)] + [InlineData("/subdir", @".", "/subdir/missing.dir/", false)] + [InlineData("", @"./", "/missing.dir", false)] + public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); + await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash); } [ConditionalTheory] @@ -66,12 +71,14 @@ namespace Microsoft.AspNetCore.StaticFiles [OSSkipCondition(OperatingSystems.MacOSX)] [InlineData("", @".\", "/missing.dir")] [InlineData("", @".\", "/Missing.dir")] - public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".\", "/missing.dir", false)] + [InlineData("", @".\", "/Missing.dir", false)] + public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); + await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash); } - private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) + private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) { @@ -79,7 +86,8 @@ namespace Microsoft.AspNetCore.StaticFiles app => app.UseDirectoryBrowser(new DirectoryBrowserOptions { RequestPath = new PathString(baseUrl), - FileProvider = fileProvider + FileProvider = fileProvider, + RedirectToAppendTrailingSlash = appendTrailingSlash }), services => services.AddDirectoryBrowser()); var response = await server.CreateRequest(requestUrl).GetAsync(); @@ -117,7 +125,7 @@ namespace Microsoft.AspNetCore.StaticFiles FileProvider = fileProvider }); - app.UseEndpoints(endpoints => {}); + app.UseEndpoints(endpoints => { }); }, services => { services.AddDirectoryBrowser(); services.AddRouting(); }); @@ -133,9 +141,19 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("/somedir", @".", "/somedir/")] [InlineData("/somedir", @"./", "/somedir/")] [InlineData("/somedir", @".", "/somedir/SubFolder/")] - public async Task FoundDirectory_Served_All(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".", "/", false)] + [InlineData("", @".", "/SubFolder/", false)] + [InlineData("/somedir", @".", "/somedir/", false)] + [InlineData("/somedir", @"./", "/somedir/", false)] + [InlineData("/somedir", @".", "/somedir/SubFolder/", false)] + [InlineData("", @".", "", false)] + [InlineData("", @".", "/SubFolder", false)] + [InlineData("/somedir", @".", "/somedir", false)] + [InlineData("/somedir", @"./", "/somedir", false)] + [InlineData("/somedir", @".", "/somedir/SubFolder", false)] + public async Task FoundDirectory_Served_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await FoundDirectory_Served(baseUrl, baseDir, requestUrl); + await FoundDirectory_Served(baseUrl, baseDir, requestUrl, appendTrailingSlash); } [ConditionalTheory] @@ -143,12 +161,16 @@ namespace Microsoft.AspNetCore.StaticFiles [OSSkipCondition(OperatingSystems.MacOSX)] [InlineData("/somedir", @".\", "/somedir/")] [InlineData("/somedir", @".", "/somedir/subFolder/")] - public async Task FoundDirectory_Served_Windows(string baseUrl, string baseDir, string requestUrl) + [InlineData("/somedir", @".\", "/somedir/", false)] + [InlineData("/somedir", @".", "/somedir/subFolder/", false)] + [InlineData("/somedir", @".\", "/somedir", false)] + [InlineData("/somedir", @".", "/somedir/subFolder", false)] + public async Task FoundDirectory_Served_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await FoundDirectory_Served(baseUrl, baseDir, requestUrl); + await FoundDirectory_Served(baseUrl, baseDir, requestUrl, appendTrailingSlash); } - private async Task FoundDirectory_Served(string baseUrl, string baseDir, string requestUrl) + private async Task FoundDirectory_Served(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) { @@ -156,7 +178,8 @@ namespace Microsoft.AspNetCore.StaticFiles app => app.UseDirectoryBrowser(new DirectoryBrowserOptions { RequestPath = new PathString(baseUrl), - FileProvider = fileProvider + FileProvider = fileProvider, + RedirectToAppendTrailingSlash = appendTrailingSlash, }), services => services.AddDirectoryBrowser()); var response = await server.CreateRequest(requestUrl).GetAsync(); @@ -215,21 +238,31 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("", @".", "/SubFolder/")] [InlineData("/somedir", @".", "/somedir/")] [InlineData("/somedir", @".", "/somedir/SubFolder/")] - public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".", "/", false)] + [InlineData("", @".", "/SubFolder/", false)] + [InlineData("/somedir", @".", "/somedir/", false)] + [InlineData("/somedir", @".", "/somedir/SubFolder/", false)] + [InlineData("", @".", "", false)] + [InlineData("", @".", "/SubFolder", false)] + [InlineData("/somedir", @".", "/somedir", false)] + [InlineData("/somedir", @".", "/somedir/SubFolder", false)] + public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); + await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash); } [ConditionalTheory] [OSSkipCondition(OperatingSystems.Linux)] [OSSkipCondition(OperatingSystems.MacOSX)] [InlineData("/somedir", @".", "/somedir/subFolder/")] - public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) + [InlineData("/somedir", @".", "/somedir/subFolder/", false)] + [InlineData("/somedir", @".", "/somedir/subFolder", false)] + public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); + await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl, appendTrailingSlash); } - private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl) + private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) { @@ -237,7 +270,8 @@ namespace Microsoft.AspNetCore.StaticFiles app => app.UseDirectoryBrowser(new DirectoryBrowserOptions { RequestPath = new PathString(baseUrl), - FileProvider = fileProvider + FileProvider = fileProvider, + RedirectToAppendTrailingSlash = appendTrailingSlash }), services => services.AddDirectoryBrowser()); @@ -251,21 +285,31 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("", @".", "/SubFolder/")] [InlineData("/somedir", @".", "/somedir/")] [InlineData("/somedir", @".", "/somedir/SubFolder/")] - public async Task HeadDirectory_HeadersButNotBodyServed_All(string baseUrl, string baseDir, string requestUrl) + [InlineData("", @".", "/", false)] + [InlineData("", @".", "/SubFolder/", false)] + [InlineData("/somedir", @".", "/somedir/", false)] + [InlineData("/somedir", @".", "/somedir/SubFolder/", false)] + [InlineData("", @".", "", false)] + [InlineData("", @".", "/SubFolder", false)] + [InlineData("/somedir", @".", "/somedir", false)] + [InlineData("/somedir", @".", "/somedir/SubFolder", false)] + public async Task HeadDirectory_HeadersButNotBodyServed_All(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl); + await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl, appendTrailingSlash); } [ConditionalTheory] [OSSkipCondition(OperatingSystems.Linux)] [OSSkipCondition(OperatingSystems.MacOSX)] [InlineData("/somedir", @".", "/somedir/subFolder/")] - public async Task HeadDirectory_HeadersButNotBodyServed_Windows(string baseUrl, string baseDir, string requestUrl) + [InlineData("/somedir", @".", "/somedir/subFolder/", false)] + [InlineData("/somedir", @".", "/somedir/subFolder", false)] + public async Task HeadDirectory_HeadersButNotBodyServed_Windows(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { - await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl); + await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl, appendTrailingSlash); } - private async Task HeadDirectory_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl) + private async Task HeadDirectory_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl, bool appendTrailingSlash = true) { using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) { @@ -273,7 +317,8 @@ namespace Microsoft.AspNetCore.StaticFiles app => app.UseDirectoryBrowser(new DirectoryBrowserOptions { RequestPath = new PathString(baseUrl), - FileProvider = fileProvider + FileProvider = fileProvider, + RedirectToAppendTrailingSlash = appendTrailingSlash }), services => services.AddDirectoryBrowser()); @@ -285,5 +330,11 @@ namespace Microsoft.AspNetCore.StaticFiles Assert.Empty((await response.Content.ReadAsByteArrayAsync())); } } + + [Fact] + public void Options_AppendTrailingSlashByDefault() + { + Assert.True(new DirectoryBrowserOptions().RedirectToAppendTrailingSlash); + } } } diff --git a/src/Middleware/WebSockets/test/ConformanceTests/Autobahn/AutobahnTester.cs b/src/Middleware/WebSockets/test/ConformanceTests/Autobahn/AutobahnTester.cs index f4b4a2fd8e..7e985a0599 100644 --- a/src/Middleware/WebSockets/test/ConformanceTests/Autobahn/AutobahnTester.cs +++ b/src/Middleware/WebSockets/test/ConformanceTests/Autobahn/AutobahnTester.cs @@ -139,7 +139,7 @@ namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn { Scheme = (ssl ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), ApplicationType = ApplicationType.Portable, - TargetFramework = "netcoreapp3.1", + TargetFramework = "netcoreapp5.0", EnvironmentName = environment, SiteName = "HttpTestSite", // This is configured in the Http.config ServerConfigTemplateContent = (server == ServerType.IISExpress) ? File.ReadAllText(configPath) : null, diff --git a/src/MusicStore/test/MusicStore.E2ETests/DotnetRunTests.cs b/src/MusicStore/test/MusicStore.E2ETests/DotnetRunTests.cs index a77a7ee26d..ed7fb66de9 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/DotnetRunTests.cs +++ b/src/MusicStore/test/MusicStore.E2ETests/DotnetRunTests.cs @@ -17,7 +17,7 @@ namespace E2ETests { public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.Kestrel) - .WithTfms(Tfm.NetCoreApp31); + .WithTfms(Tfm.NetCoreApp50); [ConditionalTheory] [MemberData(nameof(TestVariants))] diff --git a/src/MusicStore/test/MusicStore.E2ETests/NtlmAuthentationTest.cs b/src/MusicStore/test/MusicStore.E2ETests/NtlmAuthentationTest.cs index 27f1a89955..ee2973fe1b 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/NtlmAuthentationTest.cs +++ b/src/MusicStore/test/MusicStore.E2ETests/NtlmAuthentationTest.cs @@ -18,7 +18,7 @@ namespace E2ETests { public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.HttpSys) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes() .WithAllArchitectures(); diff --git a/src/MusicStore/test/MusicStore.E2ETests/OpenIdConnectTests.cs b/src/MusicStore/test/MusicStore.E2ETests/OpenIdConnectTests.cs index 72a7433d51..5e59c33c38 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/OpenIdConnectTests.cs +++ b/src/MusicStore/test/MusicStore.E2ETests/OpenIdConnectTests.cs @@ -15,7 +15,7 @@ namespace E2ETests { public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel) - .WithTfms(Tfm.NetCoreApp31); + .WithTfms(Tfm.NetCoreApp50); [ConditionalTheory] [MemberData(nameof(TestVariants))] diff --git a/src/MusicStore/test/MusicStore.E2ETests/PublishAndRunTests.cs b/src/MusicStore/test/MusicStore.E2ETests/PublishAndRunTests.cs index baee1468f5..d0760c261f 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/PublishAndRunTests.cs +++ b/src/MusicStore/test/MusicStore.E2ETests/PublishAndRunTests.cs @@ -16,7 +16,7 @@ namespace E2ETests { public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.HttpSys) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes() .WithAllHostingModels() .WithAllArchitectures(); diff --git a/src/MusicStore/test/MusicStore.E2ETests/SmokeTests.cs b/src/MusicStore/test/MusicStore.E2ETests/SmokeTests.cs index 5fd0c206d3..26d8cd2771 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/SmokeTests.cs +++ b/src/MusicStore/test/MusicStore.E2ETests/SmokeTests.cs @@ -17,7 +17,7 @@ namespace E2ETests { public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.HttpSys) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes() .WithAllHostingModels(); diff --git a/src/MusicStore/test/MusicStore.E2ETests/SmokeTestsOnNanoServer.cs b/src/MusicStore/test/MusicStore.E2ETests/SmokeTestsOnNanoServer.cs index 9d6b9dea44..490d2148ce 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/SmokeTestsOnNanoServer.cs +++ b/src/MusicStore/test/MusicStore.E2ETests/SmokeTestsOnNanoServer.cs @@ -244,7 +244,7 @@ namespace E2ETests _remoteDeploymentConfig.AccountName, _remoteDeploymentConfig.AccountPassword) { - TargetFramework = Tfm.NetCoreApp31, + TargetFramework = Tfm.NetCoreApp50, ApplicationBaseUriHint = applicationBaseUrl, ApplicationType = applicationType }; diff --git a/src/MusicStore/test/MusicStore.E2ETests/StoreSmokeTests.cs b/src/MusicStore/test/MusicStore.E2ETests/StoreSmokeTests.cs index f5f57f4c04..5995c16391 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/StoreSmokeTests.cs +++ b/src/MusicStore/test/MusicStore.E2ETests/StoreSmokeTests.cs @@ -34,7 +34,7 @@ namespace E2ETests EnvironmentName = "SocialTesting", PublishApplicationBeforeDeployment = true, PreservePublishedApplicationForDebugging = Helpers.PreservePublishedApplicationForDebugging, - TargetFramework = Tfm.NetCoreApp31, + TargetFramework = Tfm.NetCoreApp50, UserAdditionalCleanup = parameters => { DbUtils.DropDatabase(musicStoreDbName, logger); diff --git a/src/Mvc/Mvc.Abstractions/src/ModelBinding/IValueProvider.cs b/src/Mvc/Mvc.Abstractions/src/ModelBinding/IValueProvider.cs index 4e57509298..55d1f63af9 100644 --- a/src/Mvc/Mvc.Abstractions/src/ModelBinding/IValueProvider.cs +++ b/src/Mvc/Mvc.Abstractions/src/ModelBinding/IValueProvider.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// Retrieves a value object using the specified key. /// </summary> /// <param name="key">The key of the value object to retrieve.</param> - /// <returns>The value object for the specified key. If the exact key is not found, null.</returns> + /// <returns>The value object for the specified key. If the exact key is not found, <see cref="ValueProviderResult.None" />.</returns> ValueProviderResult GetValue(string key); } } diff --git a/src/Mvc/Mvc.Core/src/ControllerBase.cs b/src/Mvc/Mvc.Core/src/ControllerBase.cs index e502ca144a..3590d6146f 100644 --- a/src/Mvc/Mvc.Core/src/ControllerBase.cs +++ b/src/Mvc/Mvc.Core/src/ControllerBase.cs @@ -1848,7 +1848,7 @@ namespace Microsoft.AspNetCore.Mvc /// <summary> /// Creates an <see cref="ObjectResult"/> that produces a <see cref="ProblemDetails"/> response. /// </summary> - /// <param name="statusCode">The value for <see cref="ProblemDetails.Status" />..</param> + /// <param name="statusCode">The value for <see cref="ProblemDetails.Status" />.</param> /// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param> /// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param> /// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param> diff --git a/src/Mvc/Mvc.Core/src/FileContentResult.cs b/src/Mvc/Mvc.Core/src/FileContentResult.cs index 0c33859001..b289f20413 100644 --- a/src/Mvc/Mvc.Core/src/FileContentResult.cs +++ b/src/Mvc/Mvc.Core/src/FileContentResult.cs @@ -27,10 +27,6 @@ namespace Microsoft.AspNetCore.Mvc public FileContentResult(byte[] fileContents, string contentType) : this(fileContents, MediaTypeHeaderValue.Parse(contentType)) { - if (fileContents == null) - { - throw new ArgumentNullException(nameof(fileContents)); - } } /// <summary> @@ -80,4 +76,4 @@ namespace Microsoft.AspNetCore.Mvc return executor.ExecuteAsync(context, this); } } -}
\ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/src/Routing/UrlHelperFactory.cs b/src/Mvc/Mvc.Core/src/Routing/UrlHelperFactory.cs index dfe16c2421..5fffa77974 100644 --- a/src/Mvc/Mvc.Core/src/Routing/UrlHelperFactory.cs +++ b/src/Mvc/Mvc.Core/src/Routing/UrlHelperFactory.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing { if (context == null) { - throw new ArgumentNullException(Resources.ArgumentCannotBeNullOrEmpty, (nameof(context))); + throw new ArgumentNullException(nameof(context)); } var httpContext = context.HttpContext; @@ -69,4 +69,4 @@ namespace Microsoft.AspNetCore.Mvc.Routing return urlHelper; } } -}
\ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/src/VirtualFileResult.cs b/src/Mvc/Mvc.Core/src/VirtualFileResult.cs index af075b5a09..e44b0623ff 100644 --- a/src/Mvc/Mvc.Core/src/VirtualFileResult.cs +++ b/src/Mvc/Mvc.Core/src/VirtualFileResult.cs @@ -27,10 +27,6 @@ namespace Microsoft.AspNetCore.Mvc public VirtualFileResult(string fileName, string contentType) : this(fileName, MediaTypeHeaderValue.Parse(contentType)) { - if (fileName == null) - { - throw new ArgumentNullException(nameof(fileName)); - } } /// <summary> diff --git a/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs b/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs index e5c390395b..15d2341e12 100644 --- a/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs +++ b/src/Mvc/Mvc.Core/test/Authorization/AuthorizeFilterTest.cs @@ -317,11 +317,11 @@ namespace Microsoft.AspNetCore.Mvc.Authorization public async Task AuthorizationFilterCombinesMultipleFilters() { // Arrange - var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => true).Build()); + var authorizeFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => false).Build()); var authorizationContext = GetAuthorizationContext(anonymous: false); // Effective policy should fail, if both are combined authorizationContext.Filters.Add(authorizeFilter); - var secondFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => false).Build()); + var secondFilter = new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAssertion(a => true).Build()); authorizationContext.Filters.Add(secondFilter); // Act diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs index 411725c7a9..d6af5e8f72 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs @@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters internal override string ReadAsync_AddsModelValidationErrorsToModelState_Expected => "$.Age"; - internal override string JsonFormatter_EscapedKeys_Expected => "$[0]['It\\u0022s a key']"; + internal override string JsonFormatter_EscapedKeys_Expected => "$[0]['It\"s a key']"; internal override string JsonFormatter_EscapedKeys_Bracket_Expected => "$[0]['It[s a key']"; diff --git a/src/Mvc/Mvc.DataAnnotations/src/ValidationAttributeAdapterProvider.cs b/src/Mvc/Mvc.DataAnnotations/src/ValidationAttributeAdapterProvider.cs index 7ec543d39d..a32a28a032 100644 --- a/src/Mvc/Mvc.DataAnnotations/src/ValidationAttributeAdapterProvider.cs +++ b/src/Mvc/Mvc.DataAnnotations/src/ValidationAttributeAdapterProvider.cs @@ -29,51 +29,51 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations var type = attribute.GetType(); - if (type == typeof(RegularExpressionAttribute)) + if (typeof(RegularExpressionAttribute).IsAssignableFrom(type)) { adapter = new RegularExpressionAttributeAdapter((RegularExpressionAttribute)attribute, stringLocalizer); } - else if (type == typeof(MaxLengthAttribute)) + else if (typeof(MaxLengthAttribute).IsAssignableFrom(type)) { adapter = new MaxLengthAttributeAdapter((MaxLengthAttribute)attribute, stringLocalizer); } - else if (type == typeof(RequiredAttribute)) + else if (typeof(RequiredAttribute).IsAssignableFrom(type)) { adapter = new RequiredAttributeAdapter((RequiredAttribute)attribute, stringLocalizer); } - else if (type == typeof(CompareAttribute)) + else if (typeof(CompareAttribute).IsAssignableFrom(type)) { adapter = new CompareAttributeAdapter((CompareAttribute)attribute, stringLocalizer); } - else if (type == typeof(MinLengthAttribute)) + else if (typeof(MinLengthAttribute).IsAssignableFrom(type)) { adapter = new MinLengthAttributeAdapter((MinLengthAttribute)attribute, stringLocalizer); } - else if (type == typeof(CreditCardAttribute)) + else if (typeof(CreditCardAttribute).IsAssignableFrom(type)) { adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-creditcard", stringLocalizer); } - else if (type == typeof(StringLengthAttribute)) + else if (typeof(StringLengthAttribute).IsAssignableFrom(type)) { adapter = new StringLengthAttributeAdapter((StringLengthAttribute)attribute, stringLocalizer); } - else if (type == typeof(RangeAttribute)) + else if (typeof(RangeAttribute).IsAssignableFrom(type)) { adapter = new RangeAttributeAdapter((RangeAttribute)attribute, stringLocalizer); } - else if (type == typeof(EmailAddressAttribute)) + else if (typeof(EmailAddressAttribute).IsAssignableFrom(type)) { adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-email", stringLocalizer); } - else if (type == typeof(PhoneAttribute)) + else if (typeof(PhoneAttribute).IsAssignableFrom(type)) { adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-phone", stringLocalizer); } - else if (type == typeof(UrlAttribute)) + else if (typeof(UrlAttribute).IsAssignableFrom(type)) { adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-url", stringLocalizer); } - else if (type == typeof(FileExtensionsAttribute)) + else if (typeof(FileExtensionsAttribute).IsAssignableFrom(type)) { adapter = new FileExtensionsAttributeAdapter((FileExtensionsAttribute)attribute, stringLocalizer); } diff --git a/src/Mvc/Mvc.DataAnnotations/test/ValidationAttributeAdapterProviderTest.cs b/src/Mvc/Mvc.DataAnnotations/test/ValidationAttributeAdapterProviderTest.cs index ef6027c5a1..51d0c6f4a7 100644 --- a/src/Mvc/Mvc.DataAnnotations/test/ValidationAttributeAdapterProviderTest.cs +++ b/src/Mvc/Mvc.DataAnnotations/test/ValidationAttributeAdapterProviderTest.cs @@ -41,6 +41,10 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations { new RequiredAttribute(), typeof(RequiredAttributeAdapter) + }, + { + new CustomRegularExpressionAttribute("abc"), + typeof(RegularExpressionAttributeAdapter) } }; } @@ -85,5 +89,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations var dataTypeAdapter = Assert.IsType<DataTypeAttributeAdapter>(adapter); Assert.Equal(expectedRuleName, dataTypeAdapter.RuleName); } + + class CustomRegularExpressionAttribute : RegularExpressionAttribute + { + public CustomRegularExpressionAttribute(string pattern) : base(pattern) + { + ErrorMessage = "Not valid."; + } + } } } diff --git a/src/Mvc/Mvc.Formatters.Xml/ref/Microsoft.AspNetCore.Mvc.Formatters.Xml.netcoreapp.cs b/src/Mvc/Mvc.Formatters.Xml/ref/Microsoft.AspNetCore.Mvc.Formatters.Xml.netcoreapp.cs index fa9ffb071d..eae4a1a733 100644 --- a/src/Mvc/Mvc.Formatters.Xml/ref/Microsoft.AspNetCore.Mvc.Formatters.Xml.netcoreapp.cs +++ b/src/Mvc/Mvc.Formatters.Xml/ref/Microsoft.AspNetCore.Mvc.Formatters.Xml.netcoreapp.cs @@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters protected override bool CanReadType(System.Type type) { throw null; } protected virtual System.Xml.Serialization.XmlSerializer CreateSerializer(System.Type type) { throw null; } protected virtual System.Xml.XmlReader CreateXmlReader(System.IO.Stream readStream, System.Text.Encoding encoding) { throw null; } + protected virtual System.Xml.XmlReader CreateXmlReader(System.IO.Stream readStream, System.Text.Encoding encoding, System.Type type) { throw null; } protected virtual System.Xml.Serialization.XmlSerializer GetCachedSerializer(System.Type type) { throw null; } protected virtual System.Type GetSerializableType(System.Type declaredType) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] diff --git a/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs b/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs index 4debb6fe69..da40141ded 100644 --- a/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs +++ b/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs @@ -119,8 +119,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters try { - using var xmlReader = CreateXmlReader(readStream, encoding); - var type = GetSerializableType(context.ModelType); + var type = GetSerializableType(context.ModelType); + using var xmlReader = CreateXmlReader(readStream, encoding, type); var serializer = GetCachedSerializer(type); @@ -196,6 +196,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// </summary> /// <param name="readStream">The <see cref="Stream"/> from which to read.</param> /// <param name="encoding">The <see cref="Encoding"/> used to read the stream.</param> + /// <param name="type">The <see cref="Type"/> that is to be deserialized.</param> + /// <returns>The <see cref="XmlReader"/> used during deserialization.</returns> + protected virtual XmlReader CreateXmlReader(Stream readStream, Encoding encoding, Type type) + { + return CreateXmlReader(readStream, encoding); + } + + /// <summary> + /// Called during deserialization to get the <see cref="XmlReader"/>. + /// </summary> + /// <param name="readStream">The <see cref="Stream"/> from which to read.</param> + /// <param name="encoding">The <see cref="Encoding"/> used to read the stream.</param> /// <returns>The <see cref="XmlReader"/> used during deserialization.</returns> protected virtual XmlReader CreateXmlReader(Stream readStream, Encoding encoding) { diff --git a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/build/netcoreapp3.0/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.targets b/src/Mvc/Mvc.Razor.RuntimeCompilation/src/build/netcoreapp5.0/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.targets index f12a646391..f12a646391 100644 --- a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/build/netcoreapp3.0/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.targets +++ b/src/Mvc/Mvc.Razor.RuntimeCompilation/src/build/netcoreapp5.0/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.targets diff --git a/src/Mvc/Mvc.RazorPages/ref/Directory.Build.props b/src/Mvc/Mvc.RazorPages/ref/Directory.Build.props index bae646f06c..6cab2bf950 100644 --- a/src/Mvc/Mvc.RazorPages/ref/Directory.Build.props +++ b/src/Mvc/Mvc.RazorPages/ref/Directory.Build.props @@ -3,7 +3,7 @@ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" /> <ItemGroup> - <Compile Include="Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.1.Manual.cs" /> + <Compile Include="Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp5.0.Manual.cs" /> </ItemGroup> </Project> diff --git a/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.1.Manual.cs b/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp5.0.Manual.cs index 64141594e4..64141594e4 100644 --- a/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.1.Manual.cs +++ b/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp5.0.Manual.cs diff --git a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs index d3df4d963d..3ce7506e08 100644 --- a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs @@ -309,29 +309,32 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers "checkbox")); } - // hiddenForCheckboxTag always rendered after the returned element - var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name); - if (hiddenForCheckboxTag != null) - { - var renderingMode = - output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag; - hiddenForCheckboxTag.TagRenderMode = renderingMode; - if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") && - !string.IsNullOrEmpty(Name)) + if (ViewContext.CheckBoxHiddenInputRenderMode != CheckBoxHiddenInputRenderMode.None) + { + // hiddenForCheckboxTag always rendered after the returned element + var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name); + if (hiddenForCheckboxTag != null) { - // The checkbox and hidden elements should have the same name attribute value. Attributes will - // match if both are present because both have a generated value. Reach here in the special case - // where user provided a non-empty fallback name. - hiddenForCheckboxTag.MergeAttribute("name", Name); - } + var renderingMode = + output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag; + hiddenForCheckboxTag.TagRenderMode = renderingMode; + if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") && + !string.IsNullOrEmpty(Name)) + { + // The checkbox and hidden elements should have the same name attribute value. Attributes will + // match if both are present because both have a generated value. Reach here in the special case + // where user provided a non-empty fallback name. + hiddenForCheckboxTag.MergeAttribute("name", Name); + } - if (ViewContext.FormContext.CanRenderAtEndOfForm) - { - ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag); - } - else - { - output.PostElement.AppendHtml(hiddenForCheckboxTag); + if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.EndOfForm && ViewContext.FormContext.CanRenderAtEndOfForm) + { + ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag); + } + else + { + output.PostElement.AppendHtml(hiddenForCheckboxTag); + } } } diff --git a/src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs index 265ecde1f0..557454892b 100644 --- a/src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs @@ -839,6 +839,243 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } [Fact] + public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeNone() + { + var propertyName = "-expression-"; + var expectedTagName = "input"; + var inputTypeName = "checkbox"; + var expectedAttributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", inputTypeName }, + { "value", "true" }, + }; + + var metadataProvider = new EmptyModelMetadataProvider(); + var htmlGenerator = new TestableHtmlGenerator(metadataProvider); + var model = false; + var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model); + var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer); + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + + viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.None; + + var tagHelper = new InputTagHelper(htmlGenerator) + { + For = modelExpression, + InputTypeName = inputTypeName, + Name = propertyName, + ViewContext = viewContext, + }; + + var attributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", inputTypeName }, + }; + + var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test"); + var output = new TagHelperOutput( + expectedTagName, + new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null)) + { + TagMode = TagMode.SelfClosing, + }; + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.False(output.IsContentModified); + Assert.Equal(expectedTagName, output.TagName); + + Assert.False(viewContext.FormContext.HasEndOfFormContent); + Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement))); + } + + [Fact] + public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeInline() + { + var propertyName = "-expression-"; + var expectedTagName = "input"; + var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " + + "type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />"; + var inputTypeName = "checkbox"; + var expectedAttributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", inputTypeName }, + { "value", "true" }, + }; + + var metadataProvider = new EmptyModelMetadataProvider(); + var htmlGenerator = new TestableHtmlGenerator(metadataProvider); + var model = false; + var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model); + var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer); + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + + viewContext.FormContext.CanRenderAtEndOfForm = true; + viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.Inline; + + var tagHelper = new InputTagHelper(htmlGenerator) + { + For = modelExpression, + InputTypeName = inputTypeName, + Name = propertyName, + ViewContext = viewContext, + }; + + var attributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", inputTypeName }, + }; + + var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test"); + var output = new TagHelperOutput( + expectedTagName, + new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null)) + { + TagMode = TagMode.SelfClosing, + }; + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.False(output.IsContentModified); + Assert.Equal(expectedTagName, output.TagName); + + Assert.False(viewContext.FormContext.HasEndOfFormContent); + Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement)); + } + + [Fact] + public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm() + { + var propertyName = "-expression-"; + var expectedTagName = "input"; + var expectedEndOfFormContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " + + "type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />"; + var inputTypeName = "checkbox"; + var expectedAttributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", inputTypeName }, + { "value", "true" }, + }; + + var metadataProvider = new EmptyModelMetadataProvider(); + var htmlGenerator = new TestableHtmlGenerator(metadataProvider); + var model = false; + var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model); + var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer); + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + + viewContext.FormContext.CanRenderAtEndOfForm = true; + viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm; + + var tagHelper = new InputTagHelper(htmlGenerator) + { + For = modelExpression, + InputTypeName = inputTypeName, + Name = propertyName, + ViewContext = viewContext, + }; + + var attributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", inputTypeName }, + }; + + var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test"); + var output = new TagHelperOutput( + expectedTagName, + new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null)) + { + TagMode = TagMode.SelfClosing, + }; + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.False(output.IsContentModified); + Assert.Equal(expectedTagName, output.TagName); + + Assert.Equal(expectedEndOfFormContent, string.Join("", viewContext.FormContext.EndOfFormContent.Select(html => HtmlContentUtilities.HtmlContentToString(html)))); + Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement))); + } + + [Fact] + public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm_AndCanRenderAtEndOfFormNotSet() + { + var propertyName = "-expression-"; + var expectedTagName = "input"; + var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " + + "type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />"; + var inputTypeName = "checkbox"; + var expectedAttributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", inputTypeName }, + { "value", "true" }, + }; + + var metadataProvider = new EmptyModelMetadataProvider(); + var htmlGenerator = new TestableHtmlGenerator(metadataProvider); + var model = false; + var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model); + var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer); + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + + viewContext.FormContext.CanRenderAtEndOfForm = false; + viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm; + + var tagHelper = new InputTagHelper(htmlGenerator) + { + For = modelExpression, + InputTypeName = inputTypeName, + Name = propertyName, + ViewContext = viewContext, + }; + + var attributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", inputTypeName }, + }; + + var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test"); + var output = new TagHelperOutput( + expectedTagName, + new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null)) + { + TagMode = TagMode.SelfClosing, + }; + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.False(output.IsContentModified); + Assert.Equal(expectedTagName, output.TagName); + + Assert.False(viewContext.FormContext.HasEndOfFormContent); + Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement)); + } + + [Fact] public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters() { // Arrange diff --git a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs index e9618a4842..d607693165 100644 --- a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs +++ b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs @@ -312,6 +312,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } namespace Microsoft.AspNetCore.Mvc.Rendering { + public enum CheckBoxHiddenInputRenderMode + { + None = 0, + Inline = 1, + EndOfForm = 2, + } public enum FormMethod { Get = 0, @@ -684,6 +690,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering public ViewContext() { } public ViewContext(Microsoft.AspNetCore.Mvc.ActionContext actionContext, Microsoft.AspNetCore.Mvc.ViewEngines.IView view, Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary viewData, Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionary tempData, System.IO.TextWriter writer, Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions htmlHelperOptions) { } public ViewContext(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext, Microsoft.AspNetCore.Mvc.ViewEngines.IView view, Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary viewData, System.IO.TextWriter writer) { } + public Microsoft.AspNetCore.Mvc.Rendering.CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public bool ClientValidationEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public string ExecutingFilePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public virtual Microsoft.AspNetCore.Mvc.ViewFeatures.FormContext FormContext { get { throw null; } set { } } @@ -1068,6 +1075,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures public partial class HtmlHelperOptions { public HtmlHelperOptions() { } + public Microsoft.AspNetCore.Mvc.Rendering.CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public bool ClientValidationEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Mvc.Rendering.Html5DateRenderingMode Html5DateRenderingMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public string IdAttributeDotReplacement { get { throw null; } set { } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs index 016246f4a4..cf8ee07fd9 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs @@ -721,8 +721,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures isChecked, htmlAttributes); + if (checkbox == null) + { + return HtmlString.Empty; + } + + if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.None) + { + return checkbox; + } + var hiddenForCheckbox = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, expression); - if (checkbox == null || hiddenForCheckbox == null) + if (hiddenForCheckbox == null) { return HtmlString.Empty; } @@ -736,7 +746,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures hiddenForCheckbox.MergeAttribute("name", name); } - if (ViewContext.FormContext.CanRenderAtEndOfForm) + if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.EndOfForm && ViewContext.FormContext.CanRenderAtEndOfForm) { ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckbox); return checkbox; diff --git a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs index 952bc7b4d9..f40bdf93f6 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs @@ -56,5 +56,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// <see cref="IHtmlHelper.ValidationSummary"/> and other overloads. /// </summary> public string ValidationSummaryMessageElement { get; set; } = "span"; + + /// <summary> + /// Gets or sets the way hidden inputs are rendered for checkbox tag helpers and html helpers. + /// </summary> + public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } = CheckBoxHiddenInputRenderMode.EndOfForm; } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/CheckBoxHiddenInputRenderMode.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/CheckBoxHiddenInputRenderMode.cs new file mode 100644 index 0000000000..3d89c44d80 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/CheckBoxHiddenInputRenderMode.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.Rendering +{ + /// <summary> + /// Controls the rendering of hidden input fields when using CheckBox tag helpers or html helpers. + /// </summary> + public enum CheckBoxHiddenInputRenderMode + { + /// <summary> + /// Hidden input fields will not be automatically rendered. If checkbox is not checked, no value will be posted. + /// </summary> + None = 0, + + /// <summary> + /// Hidden input fields will be rendered inline with each checkbox. Use this for legacy ASP.NET MVC behavior. + /// </summary> + Inline = 1, + + /// <summary> + /// Hidden input fields will be rendered for each checkbox at the bottom of the form element. This is the preferred render method and default MVC behavior. + /// If <see cref="Microsoft.AspNetCore.Mvc.ViewFeatures.FormContext.CanRenderAtEndOfForm"/> is <c>false</c>, will fall back on <see cref="Inline"/>. + /// </summary> + EndOfForm = 2 + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs index 0604cfc26e..ac2d6c48c9 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs @@ -89,6 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering Html5DateRenderingMode = htmlHelperOptions.Html5DateRenderingMode; ValidationSummaryMessageElement = htmlHelperOptions.ValidationSummaryMessageElement; ValidationMessageElement = htmlHelperOptions.ValidationMessageElement; + CheckBoxHiddenInputRenderMode = htmlHelperOptions.CheckBoxHiddenInputRenderMode; } /// <summary> @@ -131,6 +132,8 @@ namespace Microsoft.AspNetCore.Mvc.Rendering Html5DateRenderingMode = viewContext.Html5DateRenderingMode; ValidationSummaryMessageElement = viewContext.ValidationSummaryMessageElement; ValidationMessageElement = viewContext.ValidationMessageElement; + CheckBoxHiddenInputRenderMode = viewContext.CheckBoxHiddenInputRenderMode; + ExecutingFilePath = viewContext.ExecutingFilePath; View = view; ViewData = viewData; @@ -185,6 +188,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering public string ValidationMessageElement { get; set; } /// <summary> + /// Gets or sets the way hidden inputs are rendered for checkbox tag helpers and html helpers. + /// </summary> + public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } + + /// <summary> /// Gets the dynamic view bag. /// </summary> public dynamic ViewBag diff --git a/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperCheckboxTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperCheckboxTest.cs index 67cfb7f64e..ab35762255 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperCheckboxTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperCheckboxTest.cs @@ -170,6 +170,93 @@ namespace Microsoft.AspNetCore.Mvc.Rendering } [Fact] + public void CheckBox_WithHiddenInputRenderModeNone_DoesNotGenerateHiddenInput() + { + // Arrange + var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean"); + var expected = @"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " + + $@"data-val-required=""HtmlEncode[[{requiredMessage}]]"" id=""HtmlEncode[[Property1]]"" " + + @"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " + + @"value=""HtmlEncode[[true]]"" />"; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData()); + helper.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.None; + + // Act + var html = helper.CheckBox("Property1", isChecked: true, htmlAttributes: null); + + // Assert + Assert.False(helper.ViewContext.FormContext.HasEndOfFormContent); + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html)); + } + + [Fact] + public void CheckBox_WithHiddenInputRenderModeInline_GeneratesHiddenInput() + { + // Arrange + var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean"); + var expected = @"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " + + $@"data-val-required=""HtmlEncode[[{requiredMessage}]]"" id=""HtmlEncode[[Property1]]"" " + + @"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " + + @"value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />"; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData()); + helper.ViewContext.FormContext.CanRenderAtEndOfForm = true; + helper.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.Inline; + + // Act + var html = helper.CheckBox("Property1", isChecked: true, htmlAttributes: null); + + // Assert + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html)); + } + + [Fact] + public void CheckBox_WithHiddenInputRenderModeEndOfForm_GeneratesHiddenInput() + { + // Arrange + var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean"); + var expected = @"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " + + $@"data-val-required=""HtmlEncode[[{requiredMessage}]]"" id=""HtmlEncode[[Property1]]"" " + + @"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " + + @"value=""HtmlEncode[[true]]"" />"; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData()); + helper.ViewContext.FormContext.CanRenderAtEndOfForm = true; + helper.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm; + + // Act + var html = helper.CheckBox("Property1", isChecked: true, htmlAttributes: null); + + // Assert + Assert.True(helper.ViewContext.FormContext.HasEndOfFormContent); + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html)); + var writer = new StringWriter(); + var hiddenTag = Assert.Single(helper.ViewContext.FormContext.EndOfFormContent); + hiddenTag.WriteTo(writer, new HtmlTestEncoder()); + Assert.Equal("<input name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />", + writer.ToString()); + } + + [Fact] + public void CheckBox_WithHiddenInputRenderModeEndOfForm_WithCanRenderAtEndOfFormNotSet_GeneratesHiddenInput() + { + // Arrange + var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean"); + var expected = @"<input checked=""HtmlEncode[[checked]]"" data-val=""HtmlEncode[[true]]"" " + + $@"data-val-required=""HtmlEncode[[{requiredMessage}]]"" id=""HtmlEncode[[Property1]]"" " + + @"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[checkbox]]"" " + + @"value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />"; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData()); + helper.ViewContext.FormContext.CanRenderAtEndOfForm = false; + helper.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm; + + // Act + var html = helper.CheckBox("Property1", isChecked: true, htmlAttributes: null); + + // Assert + Assert.False(helper.ViewContext.FormContext.HasEndOfFormContent); + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html)); + } + + [Fact] public void CheckBoxUsesAttemptedValueFromModelState() { // Arrange diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json index 12fe72720c..8a669b652a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json @@ -337,12 +337,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json index 96ac0383be..08c6bb56c8 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json @@ -86,12 +86,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json index 1102d7952d..7c9eb6c777 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json @@ -82,12 +82,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json index ff8ed336b0..0b8a573d28 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json @@ -41,11 +41,11 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "defaultValue": "netcoreapp3.1" + "defaultValue": "netcoreapp5.0" }, "ExcludeLaunchSettings": { "type": "parameter", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json index 1b4166341a..86a7607fa2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json @@ -4,8 +4,7 @@ "classifications": [ "Web", "Razor", - "Library", - "Razor Class Library" + "Library" ], "name": "Razor Class Library", "generatorVersions": "[1.0.0.0-*)", @@ -48,11 +47,11 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "defaultValue": "netcoreapp3.1" + "defaultValue": "netcoreapp5.0" }, "HostIdentifier": { "type": "bind", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json index 5a01244a34..90e8b01562 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json @@ -310,12 +310,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js index 3c76e6dc45..ac49c18641 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js @@ -1,4 +1,4 @@ // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification // for details on configuring this project to bundle and minify static web assets. -// Write your Javascript code. +// Write your JavaScript code. diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json index d01fcbf4f0..7f4d444645 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json @@ -300,12 +300,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json index 5ac6316167..487cdfc527 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json @@ -87,12 +87,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js index 3c76e6dc45..ac49c18641 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js @@ -1,4 +1,4 @@ // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification // for details on configuring this project to bundle and minify static web assets. -// Write your Javascript code. +// Write your JavaScript code. diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json index b0facf26da..2de35fc292 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json @@ -209,12 +209,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json index 514e77d41b..e1639b58b9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json @@ -82,12 +82,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json index 1e9cc2414b..16156aef0d 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json @@ -47,12 +47,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "copyrightYear": { "type": "generated", diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json index 07b982ad9e..f0162aa616 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json @@ -177,12 +177,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "HostIdentifier": { "type": "bind", diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/api-authorization/authorize.service.ts b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/api-authorization/authorize.service.ts index cf6e33cfda..779eb24850 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/api-authorization/authorize.service.ts +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/api-authorization/authorize.service.ts @@ -135,7 +135,7 @@ export class AuthorizeService { await this.userManager.signoutRedirect(this.createArguments(state)); return this.redirect(); } catch (redirectSignOutError) { - console.log('Redirect signout error: ', popupSignOutError); + console.log('Redirect signout error: ', redirectSignOutError); return this.error(redirectSignOutError); } } diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/OidcConfigurationController.cs b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/OidcConfigurationController.cs index 75e26da4e9..cdcc89182a 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/OidcConfigurationController.cs +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/OidcConfigurationController.cs @@ -6,12 +6,12 @@ namespace Company.WebApplication1.Controllers { public class OidcConfigurationController : Controller { - private readonly ILogger<OidcConfigurationController> logger; + private readonly ILogger<OidcConfigurationController> _logger; - public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger<OidcConfigurationController> _logger) + public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger<OidcConfigurationController> logger) { ClientRequestParametersProvider = clientRequestParametersProvider; - logger = _logger; + _logger = logger; } public IClientRequestParametersProvider ClientRequestParametersProvider { get; } diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json index c6e9544e43..6490d8e061 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json @@ -178,12 +178,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "HostIdentifier": { "type": "bind", diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.env b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.env new file mode 100644 index 0000000000..6ce384e5ce --- /dev/null +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.env @@ -0,0 +1 @@ +BROWSER=none diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json index 9c7ce60528..0f2945d10f 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json @@ -87,12 +87,12 @@ "datatype": "choice", "choices": [ { - "choice": "netcoreapp3.1", - "description": "Target netcoreapp3.1" + "choice": "netcoreapp5.0", + "description": "Target netcoreapp5.0" } ], - "replaces": "netcoreapp3.1", - "defaultValue": "netcoreapp3.1" + "replaces": "netcoreapp5.0", + "defaultValue": "netcoreapp5.0" }, "HostIdentifier": { "type": "bind", diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.env b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.env new file mode 100644 index 0000000000..6ce384e5ce --- /dev/null +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.env @@ -0,0 +1 @@ +BROWSER=none diff --git a/src/ProjectTemplates/scripts/Run-Angular-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Angular-Locally.ps1 index ba4180de95..ec8993da7e 100644 --- a/src/ProjectTemplates/scripts/Run-Angular-Locally.ps1 +++ b/src/ProjectTemplates/scripts/Run-Angular-Locally.ps1 @@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot\Test-Template.ps1 -Test-Template "angular" "angular" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.1.3.1.0-dev.nupkg" $true +Test-Template "angular" "angular" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true diff --git a/src/ProjectTemplates/scripts/Run-Blazor-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Blazor-Locally.ps1 index 7d989b65a3..575036e9c7 100644 --- a/src/ProjectTemplates/scripts/Run-Blazor-Locally.ps1 +++ b/src/ProjectTemplates/scripts/Run-Blazor-Locally.ps1 @@ -10,4 +10,4 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot\Test-Template.ps1 -Test-Template "blazorserver" "blazorserver" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false +Test-Template "blazorserver" "blazorserver" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-EmptyWeb-Locally.ps1 b/src/ProjectTemplates/scripts/Run-EmptyWeb-Locally.ps1 index 6aeffd7687..d6859c6f72 100644 --- a/src/ProjectTemplates/scripts/Run-EmptyWeb-Locally.ps1 +++ b/src/ProjectTemplates/scripts/Run-EmptyWeb-Locally.ps1 @@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot\Test-Template.ps1 -Test-Template "web" "web" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false +Test-Template "web" "web" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-Razor-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Razor-Locally.ps1 index f77838b435..ecba4fbd8f 100644 --- a/src/ProjectTemplates/scripts/Run-Razor-Locally.ps1 +++ b/src/ProjectTemplates/scripts/Run-Razor-Locally.ps1 @@ -6,4 +6,4 @@ param() . $PSScriptRoot\Test-Template.ps1 -Test-Template "webapp" "webapp -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false +Test-Template "webapp" "webapp -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-React-Locally.ps1 b/src/ProjectTemplates/scripts/Run-React-Locally.ps1 index 972bfc4ead..8308860edc 100644 --- a/src/ProjectTemplates/scripts/Run-React-Locally.ps1 +++ b/src/ProjectTemplates/scripts/Run-React-Locally.ps1 @@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot\Test-Template.ps1 -Test-Template "react" "react" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.1.3.1.0-dev.nupkg" $true +Test-Template "react" "react" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true diff --git a/src/ProjectTemplates/scripts/Run-ReactRedux-Locally.ps1 b/src/ProjectTemplates/scripts/Run-ReactRedux-Locally.ps1 index 4c01f11400..6100d7cacd 100644 --- a/src/ProjectTemplates/scripts/Run-ReactRedux-Locally.ps1 +++ b/src/ProjectTemplates/scripts/Run-ReactRedux-Locally.ps1 @@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot\Test-Template.ps1 -Test-Template "reactredux" "reactredux" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.1.3.1.0-dev.nupkg" $true +Test-Template "reactredux" "reactredux" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true diff --git a/src/ProjectTemplates/scripts/Run-Starterweb-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Starterweb-Locally.ps1 index 7971ae01ac..61ad47fbdc 100644 --- a/src/ProjectTemplates/scripts/Run-Starterweb-Locally.ps1 +++ b/src/ProjectTemplates/scripts/Run-Starterweb-Locally.ps1 @@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot\Test-Template.ps1 -Test-Template "mvc" "mvc -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false +Test-Template "mvc" "mvc -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Run-Worker-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Worker-Locally.ps1 index 94fd8d012b..e6ff856e7e 100644 --- a/src/ProjectTemplates/scripts/Run-Worker-Locally.ps1 +++ b/src/ProjectTemplates/scripts/Run-Worker-Locally.ps1 @@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot\Test-Template.ps1 -Test-Template "worker" "worker" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false +Test-Template "worker" "worker" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/scripts/Test-Template.ps1 b/src/ProjectTemplates/scripts/Test-Template.ps1 index d13b7e452c..5ad25a16d6 100644 --- a/src/ProjectTemplates/scripts/Test-Template.ps1 +++ b/src/ProjectTemplates/scripts/Test-Template.ps1 @@ -32,7 +32,7 @@ function Test-Template($templateName, $templateArgs, $templateNupkg, $isSPA) { $proj = "$tmpDir/$templateName.$extension" $projContent = Get-Content -Path $proj -Raw $projContent = $projContent -replace ('<Project Sdk="Microsoft.NET.Sdk.Web">', "<Project Sdk=""Microsoft.NET.Sdk.Web""> - <Import Project=""$PSScriptRoot/../test/bin/Debug/netcoreapp3.1/TestTemplates/TemplateTests.props"" /> + <Import Project=""$PSScriptRoot/../test/bin/Debug/netcoreapp5.0/TestTemplates/TemplateTests.props"" /> <ItemGroup> <PackageReference Include=""Microsoft.NET.Sdk.Razor"" Version=""`$(MicrosoftNETSdkRazorPackageVersion)"" /> </ItemGroup> @@ -42,7 +42,7 @@ function Test-Template($templateName, $templateArgs, $templateNupkg, $isSPA) { $projContent | Set-Content $proj dotnet.exe ef migrations add mvc dotnet.exe publish --configuration Release - dotnet.exe bin\Release\netcoreapp3.1\publish\$templateName.dll + dotnet.exe bin\Release\netcoreapp5.0\publish\$templateName.dll } finally { Pop-Location diff --git a/src/ProjectTemplates/test/EmptyWebTemplateTest.cs b/src/ProjectTemplates/test/EmptyWebTemplateTest.cs index 56e2e8c105..8c0c9c899c 100644 --- a/src/ProjectTemplates/test/EmptyWebTemplateTest.cs +++ b/src/ProjectTemplates/test/EmptyWebTemplateTest.cs @@ -41,6 +41,12 @@ namespace Templates.Test var createResult = await Project.RunDotNetNewAsync("web", language: languageOverride); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult)); + // Avoid the F# compiler. See https://github.com/aspnet/AspNetCore/issues/14022 + if (languageOverride != null) + { + return; + } + var publishResult = await Project.RunDotNetPublishAsync(); Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult)); diff --git a/src/ProjectTemplates/test/Helpers/Project.cs b/src/ProjectTemplates/test/Helpers/Project.cs index fc6923ae5c..8d8b4ddb7b 100644 --- a/src/ProjectTemplates/test/Helpers/Project.cs +++ b/src/ProjectTemplates/test/Helpers/Project.cs @@ -21,7 +21,7 @@ namespace Templates.Test.Helpers { private const string _urls = "http://127.0.0.1:0;https://127.0.0.1:0"; - public const string DefaultFramework = "netcoreapp3.1"; + public const string DefaultFramework = "netcoreapp5.0"; public static bool IsCIEnvironment => typeof(Project).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>() .Any(a => a.Key == "ContinuousIntegrationBuild"); diff --git a/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs b/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs index 2c34259592..f9de81d768 100644 --- a/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs +++ b/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs @@ -32,10 +32,12 @@ namespace Templates.Test.Helpers "Microsoft.DotNet.Web.ProjectTemplates.2.2", "Microsoft.DotNet.Web.ProjectTemplates.3.0", "Microsoft.DotNet.Web.ProjectTemplates.3.1", + "Microsoft.DotNet.Web.ProjectTemplates.5.0", "Microsoft.DotNet.Web.Spa.ProjectTemplates.2.1", "Microsoft.DotNet.Web.Spa.ProjectTemplates.2.2", "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.0", "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.1", + "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0", "Microsoft.DotNet.Web.Spa.ProjectTemplates" }; @@ -90,7 +92,7 @@ namespace Templates.Test.Helpers /* * The templates are indexed by path, for example: - &USERPROFILE%\.templateengine\dotnetcli\v3.0.100-preview7-012821\packages\nunit3.dotnetnew.template.1.6.1.nupkg + &USERPROFILE%\.templateengine\dotnetcli\v5.0.100-alpha1-013788\packages\nunit3.dotnetnew.template.1.6.1.nupkg Templates: NUnit 3 Test Project (nunit) C# NUnit 3 Test Item (nunit-test) C# @@ -99,7 +101,7 @@ namespace Templates.Test.Helpers NUnit 3 Test Project (nunit) VB NUnit 3 Test Item (nunit-test) VB Uninstall Command: - dotnet new -u &USERPROFILE%\.templateengine\dotnetcli\v3.0.100-preview7-012821\packages\nunit3.dotnetnew.template.1.6.1.nupkg + dotnet new -u &USERPROFILE%\.templateengine\dotnetcli\v5.0.100-alpha1-013788\packages\nunit3.dotnetnew.template.1.6.1.nupkg * We don't want to construct this path so we'll rely on dotnet new --uninstall --help to construct the uninstall command. */ diff --git a/src/ProjectTemplates/test/Infrastructure/Directory.Build.props.in b/src/ProjectTemplates/test/Infrastructure/Directory.Build.props.in index 6186fc2f15..9990532b1d 100644 --- a/src/ProjectTemplates/test/Infrastructure/Directory.Build.props.in +++ b/src/ProjectTemplates/test/Infrastructure/Directory.Build.props.in @@ -1,3 +1,6 @@ <Project> <!-- This file gets copied above the template test projects so that we disconnect the templates from the rest of the repository --> + <PropertyGroup> + <TargetFramework>netcoreapp5.0</TargetFramework> + </PropertyGroup> </Project> diff --git a/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in b/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in index 11b43cc0cb..e012ba42b7 100644 --- a/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in +++ b/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in @@ -5,8 +5,6 @@ <!-- This sets an option which prevents the tests from rolling forward into a newer shared framework. --> <UserRuntimeConfig>$(MSBuildThisFileDirectory)runtimeconfig.norollforward.json</UserRuntimeConfig> - <!-- Workaround https://github.com/dotnet/cli/issues/10528 --> - <BundledNETCorePlatformsPackageVersion>${MicrosoftNETCorePlatformsPackageVersion}</BundledNETCorePlatformsPackageVersion> </PropertyGroup> <ItemGroup> diff --git a/src/ProjectTemplates/test/MvcTemplateTest.cs b/src/ProjectTemplates/test/MvcTemplateTest.cs index 9ebaf1afc6..9fc1ab4a04 100644 --- a/src/ProjectTemplates/test/MvcTemplateTest.cs +++ b/src/ProjectTemplates/test/MvcTemplateTest.cs @@ -47,6 +47,12 @@ namespace Templates.Test Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools.DotNet", projectFileContents); Assert.DoesNotContain("Microsoft.Extensions.SecretManager.Tools", projectFileContents); + // Avoid the F# compiler. See https://github.com/aspnet/AspNetCore/issues/14022 + if (languageOverride != null) + { + return; + } + var publishResult = await Project.RunDotNetPublishAsync(); Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult)); diff --git a/src/ProjectTemplates/test/ProjectTemplates.Tests.csproj b/src/ProjectTemplates/test/ProjectTemplates.Tests.csproj index b8797c1345..f755b3b8f2 100644 --- a/src/ProjectTemplates/test/ProjectTemplates.Tests.csproj +++ b/src/ProjectTemplates/test/ProjectTemplates.Tests.csproj @@ -62,6 +62,7 @@ <_Parameter2>true</_Parameter2> </AssemblyAttribute> </ItemGroup> + <Target Name="PrepareForTest" BeforeTargets="CoreCompile" Condition="$(DesignTimeBuild) != true"> <PropertyGroup> <TestTemplateCreationFolder>$([MSBuild]::NormalizePath('$(OutputPath)$(TestTemplateCreationFolder)'))</TestTemplateCreationFolder> @@ -79,7 +80,7 @@ <_Parameter1>ArtifactsLogDir</_Parameter1> <_Parameter2>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log'))</_Parameter2> </AssemblyAttribute> - + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> <_Parameter1>ArtifactsNonShippingPackagesDir</_Parameter1> <_Parameter2>$(ArtifactsNonShippingPackagesDir)</_Parameter2> diff --git a/src/ProjectTemplates/test/WebApiTemplateTest.cs b/src/ProjectTemplates/test/WebApiTemplateTest.cs index 89d047a06e..e97e165961 100644 --- a/src/ProjectTemplates/test/WebApiTemplateTest.cs +++ b/src/ProjectTemplates/test/WebApiTemplateTest.cs @@ -35,6 +35,12 @@ namespace Templates.Test var createResult = await Project.RunDotNetNewAsync("webapi", language: languageOverride); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult)); + // Avoid the F# compiler. See https://github.com/aspnet/AspNetCore/issues/14022 + if (languageOverride != null) + { + return; + } + var publishResult = await Project.RunDotNetPublishAsync(); Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult)); diff --git a/src/ProjectTemplates/test/template-baselines.json b/src/ProjectTemplates/test/template-baselines.json index d934d8b545..618bb27a1f 100644 --- a/src/ProjectTemplates/test/template-baselines.json +++ b/src/ProjectTemplates/test/template-baselines.json @@ -1209,6 +1209,7 @@ "ClientApp/src/App.test.js", "ClientApp/src/index.js", "ClientApp/src/registerServiceWorker.js", + "ClientApp/.env", "ClientApp/.gitignore", "ClientApp/package-lock.json", "ClientApp/package.json", @@ -1251,6 +1252,7 @@ "ClientApp/src/index.tsx", "ClientApp/src/react-app-env.d.ts", "ClientApp/src/registerServiceWorker.ts", + "ClientApp/.env", "ClientApp/.eslintrc.json", "ClientApp/.gitignore", "ClientApp/package-lock.json", diff --git a/src/Security/Authentication/Certificate/src/README.md b/src/Security/Authentication/Certificate/src/README.md index 542131fdf1..b5654819e6 100644 --- a/src/Security/Authentication/Certificate/src/README.md +++ b/src/Security/Authentication/Certificate/src/README.md @@ -1,31 +1,22 @@ # Microsoft.AspNetCore.Authentication.Certificate -This project sort of contains an implementation of [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core. -Certificate authentication happens at the TLS level, long before it ever gets to ASP.NET Core, so, more accurately this is an authentication handler -that validates the certificate and then gives you an event where you can resolve that certificate to a ClaimsPrincipal. +This project sort of contains an implementation of [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core. Certificate authentication happens at the TLS level, long before it ever gets to ASP.NET Core, so, more accurately this is an authentication handler that validates the certificate and then gives you an event where you can resolve that certificate to a ClaimsPrincipal. -You **must** [configure your host](#hostConfiguration) for certificate authentication, be it IIS, Kestrel, Azure Web Applications or whatever else you're using. +You **must** [configure your host](#configuring-your-host-to-require-certificates) for certificate authentication, be it IIS, Kestrel, Azure Web Applications or whatever else you're using. ## Getting started -First acquire an HTTPS certificate, apply it and then [configure your host](#hostConfiguration) to require certificates. +First acquire an HTTPS certificate, apply it and then [configure your host](#configuring-your-host-to-require-certificates) to require certificates. -In your web application add a reference to the package, then in the `ConfigureServices` method in `startup.cs` call -`app.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).UseCertificateAuthentication(...);` with your options, -providing a delegate for `OnValidateCertificate` to validate the client certificate sent with requests and turn that information -into an `ClaimsPrincipal`, set it on the `context.Principal` property and call `context.Success()`. +In your web application add a reference to the package, then in the `ConfigureServices` method in `startup.cs` call `app.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).UseCertificateAuthentication(...);` with your options, providing a delegate for `OnValidateCertificate` to validate the client certificate sent with requests and turn that information into an `ClaimsPrincipal`, set it on the `context.Principal` property and call `context.Success()`. -If you change your scheme name in the options for the authentication handler you need to change the scheme name in -`AddAuthentication()` to ensure it's used on every request which ends in an endpoint that requires authorization. +If you change your scheme name in the options for the authentication handler you need to change the scheme name in `AddAuthentication()` to ensure it's used on every request which ends in an endpoint that requires authorization. -If authentication fails this handler will return a `403 (Forbidden)` response rather a `401 (Unauthorized)` as you -might expect - this is because the authentication should happen during the initial TLS connection - by the time it -reaches the handler it's too late, and there's no way to actually upgrade the connection from an anonymous connection -to one with a certificate. +If authentication fails this handler will return a `403 (Forbidden)` response rather a `401 (Unauthorized)` as you might expect - this is because the authentication should happen during the initial TLS connection - by the time it reaches the handler it's too late, and there's no way to actually upgrade the connection from an anonymous connection to one with a certificate. You must also add `app.UseAuthentication();` in the `Configure` method, otherwise nothing will ever get called. -For example; +For example: ```c# public void ConfigureServices(IServiceCollection services) @@ -47,25 +38,19 @@ In the sample above you can see the default way to add certificate authenticatio ## Configuring Certificate Validation -The `CertificateAuthenticationOptions` handler has some built in validations that are the minimium validations you should perform on -a certificate. Each of these settings are turned on by default. +The `CertificateAuthenticationOptions` handler has some built in validations that are the minimum validations you should perform on a certificate. Each of these settings are turned on by default. ### ValidateCertificateChain -This check validates that the issuer for the certificate is trusted by the application host OS. If -you are going to accept self-signed certificates you must disable this check. +This check validates that the issuer for the certificate is trusted by the application host OS. If you are going to accept self-signed certificates you must disable this check. ### ValidateCertificateUse -This check validates that the certificate presented by the client has the Client Authentication -extended key use, or no EKUs at all (as the specifications say if no EKU is specified then all EKUs -are valid). +This check validates that the certificate presented by the client has the Client Authentication extended key use, or no EKUs at all (as the specifications say if no EKU is specified then all EKUs are valid). ### ValidateValidityPeriod -This check validates that the certificate is within its validity period. As the handler runs on every -request this ensures that a certificate that was valid when it was presented has not expired during -its current session. +This check validates that the certificate is within its validity period. As the handler runs on every request this ensures that a certificate that was valid when it was presented has not expired during its current session. ### RevocationFlag @@ -73,24 +58,21 @@ A flag which specifies which certificates in the chain are checked for revocatio Revocation checks are only performed when the certificate is chained to a root certificate. -### RevocationMode +### RevocationMode A flag which specifies how revocation checks are performed. + Specifying an on-line check can result in a long delay while the certificate authority is contacted. Revocation checks are only performed when the certificate is chained to a root certificate. ### Can I configure my application to require a certificate only on certain paths? -Not possible, remember the certificate exchange is done that the start of the HTTPS conversation, -it's done by the host, not the application. Kestrel, IIS, Azure Web Apps don't have any configuration for -this sort of thing. +Not possible, remember the certificate exchange is done that the start of the HTTPS conversation, it's done by the host, not the application. Kestrel, IIS, Azure Web Apps don't have any configuration for this sort of thing. -# Handler events +## Handler events -The handler has two events, `OnAuthenticationFailed()`, which is called if an exception happens during authentication and allows you to react, and `OnValidateCertificate()` which is -called after certificate has been validated, passed validation, abut before the default principal has been created. This allows you to perform your own validation, for example -checking if the certificate is one your services knows about, and to construct your own principal. For example, +The handler has two events, `OnAuthenticationFailed()`, which is called if an exception happens during authentication and allows you to react, and `OnValidateCertificate()` which is called after certificate has been validated, passed validation, abut before the default principal has been created. This allows you to perform your own validation, for example checking if the certificate is one your services knows about, and to construct your own principal. For example: ```c# services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) @@ -117,8 +99,7 @@ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationSchem If you find the inbound certificate doesn't meet your extra validation call `context.Fail("failure Reason")` with a failure reason. -For real functionality you will probably want to call a service registered in DI which talks to a database or other type of -user store. You can grab your service by using the context passed into your delegates, like so +For real functionality you will probably want to call a service registered in DI which talks to a database or other type of user store. You can grab your service by using the context passed into your delegates, like so ```c# services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) @@ -130,7 +111,7 @@ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationSchem { var validationService = context.HttpContext.RequestServices.GetService<ICertificateValidationService>(); - + if (validationService.ValidateCertificate(context.ClientCertificate)) { var claims = new[] @@ -141,17 +122,18 @@ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationSchem context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); - } + } return Task.CompletedTask; } }; }); ``` + Note that conceptually the validation of the certification is an authorization concern, and putting a check on, for example, an issuer or thumbprint in an authorization policy rather than inside OnCertificateValidated() is perfectly acceptable. -## <a name="hostConfiguration"></a>Configuring your host to require certificates +## Configuring your host to require certificates ### Kestrel @@ -170,12 +152,12 @@ public static IWebHost BuildWebHost(string[] args) }) .Build(); ``` -You must set the `ClientCertificateValidation` delegate to `CertificateValidator.DisableChannelValidation` in order to stop Kestrel using the default OS certificate validation routine and, -instead, letting the authentication handler perform the validation. + +You must set the `ClientCertificateValidation` delegate to `CertificateValidator.DisableChannelValidation` in order to stop Kestrel using the default OS certificate validation routine and, instead, letting the authentication handler perform the validation. ### IIS -In the IIS Manager +In the IIS Manager: 1. Select your Site in the Connections tab. 2. Double click the SSL Settings in the Features View window. @@ -185,9 +167,7 @@ In the IIS Manager ### Azure -See the [Azure documentation](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth) -to configure Azure Web Apps then add the following to your application startup method, `Configure(IApplicationBuilder app)` add the -following line before the call to `app.UseAuthentication();` +See the [Azure documentation](https://docs.microsoft.com/azure/app-service/app-service-web-configure-tls-mutual-auth) to configure Azure Web Apps then add the following to your application startup method, `Configure(IApplicationBuilder app)` add the following line before the call to `app.UseAuthentication();`: ```c# app.UseCertificateHeaderForwarding(); @@ -195,18 +175,13 @@ app.UseCertificateHeaderForwarding(); ### Random custom web proxies -If you're using a proxy which isn't IIS or Azure's Web Apps Application Request Routing you will need to configure your proxy -to forward the certificate it received in an HTTP header. -In your application startup method, `Configure(IApplicationBuilder app)`, add the -following line before the call to `app.UseAuthentication();` +If you're using a proxy which isn't IIS or Azure's Web Apps Application Request Routing you will need to configure your proxy to forward the certificate it received in an HTTP header. In your application startup method, `Configure(IApplicationBuilder app)`, add the following line before the call to `app.UseAuthentication();`: ```c# app.UseCertificateForwarding(); ``` -You will also need to configure the Certificate Forwarding middleware to specify the header name. -In your service configuration method, `ConfigureServices(IServiceCollection services)` add -the following code to configure the header the forwarding middleware will build a certificate from; +You will also need to configure the Certificate Forwarding middleware to specify the header name. In your service configuration method, `ConfigureServices(IServiceCollection services)` add the following code to configure the header the forwarding middleware will build a certificate from: ```c# services.AddCertificateForwarding(options => @@ -215,9 +190,7 @@ services.AddCertificateForwarding(options => }); ``` -Finally, if your proxy is doing something weird to pass the header on, rather than base 64 encoding it -(looking at you nginx (╯°□°)╯︵ ┻━┻) you can override the converter option to be a func that will -perform the optional conversion, for example +Finally, if your proxy is doing something weird to pass the header on, rather than base 64 encoding it (looking at you nginx (╯°□°)╯︵ ┻━┻) you can override the converter option to be a func that will perform the optional conversion, for example ```c# services.AddCertificateForwarding(options => @@ -231,4 +204,3 @@ services.AddCertificateForwarding(options => } }); ``` - diff --git a/src/Security/Authentication/MicrosoftAccount/src/MicrosoftChallengeProperties.cs b/src/Security/Authentication/MicrosoftAccount/src/MicrosoftChallengeProperties.cs index 8625e3f093..4e9737b509 100644 --- a/src/Security/Authentication/MicrosoftAccount/src/MicrosoftChallengeProperties.cs +++ b/src/Security/Authentication/MicrosoftAccount/src/MicrosoftChallengeProperties.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authentication.OAuth; namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount { /// <summary> - /// See https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code for reference + /// See https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code for reference /// </summary> public class MicrosoftChallengeProperties : OAuthChallengeProperties { diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md index e263a2c5f7..ec83949331 100644 --- a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md +++ b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md @@ -1,24 +1,24 @@ Cross Machine Tests -Kerberos can only be tested in a multi-machine environment. On localhost it always falls back to NTLM which has different requirements. Multi-machine is also neccisary for interop testing across OSs. Kerberos also requires domain controler SPN configuration so we can't test it on arbitrary test boxes. +Kerberos can only be tested in a multi-machine environment. On localhost it always falls back to NTLM which has different requirements. Multi-machine is also necessary for interop testing across OSs. Kerberos also requires domain controller SPN configuration so we can't test it on arbitrary test boxes. Test structure: - A remote test server with various endpoints with different authentication restrictions. -- A remote test client with endpoints that execute specific scenarios. The input for these endpoints is theory data. The output is either 200Ok, or a failure code and desciption. +- A remote test client with endpoints that execute specific scenarios. The input for these endpoints is theory data. The output is either 200Ok, or a failure code and description. - The CrossMachineTest class that drives the tests. It invokes the client app with the theory data and confirms the results. -We use these three components beceause it allows us to run the tests from a dev machine or CI agent that is not part of the dedicated test domain/environment. +We use these three components because it allows us to run the tests from a dev machine or CI agent that is not part of the dedicated test domain/environment. (Static) Environment Setup: - Warning, this environment can take a day to set up. That's why we want a static test environment that we can re-use. - Create a Windows server running DNS and Active Directory. Promote it to a domain controller. - Create an SPN on this machine for Windows -> Windows testing. `setspn -S "http/chrross-dc.crkerberos.com" -U administrator` - Future: Can we replace the domain controller with an AAD instance? We'd still want a second windows machine for Windows -> Windows testing, but AAD might be easier to configure. - - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-getting-started - - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-join-ubuntu-linux-vm - - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-enable-kcd + - https://docs.microsoft.com/azure/active-directory-domain-services/active-directory-ds-getting-started + - https://docs.microsoft.com/azure/active-directory-domain-services/active-directory-ds-join-ubuntu-linux-vm + - https://docs.microsoft.com/azure/active-directory-domain-services/active-directory-ds-enable-kcd - Create another Windows machine and join it to the test domain. -- Create a Linux machine and joing it to the domain. Ubuntu 18.04 has been used in the past. +- Create a Linux machine and joining it to the domain. Ubuntu 18.04 has been used in the past. - https://www.safesquid.com/content-filtering/integrating-linux-host-windows-ad-kerberos-sso-authentication - Include an HTTP SPN diff --git a/src/Security/README.md b/src/Security/README.md index 0ba28c1e97..5ed702c4f2 100644 --- a/src/Security/README.md +++ b/src/Security/README.md @@ -3,9 +3,9 @@ ASP.NET Core Security Contains the security and authorization middlewares for ASP.NET Core. -A list of community projects related to authentication and security for ASP.NET Core are listed in the [documentation](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/community). +A list of community projects related to authentication and security for ASP.NET Core are listed in the [documentation](https://docs.microsoft.com/aspnet/core/security/authentication/community). -See the [ASP.NET Core security documentation](https://docs.microsoft.com/en-us/aspnet/core/security/). +See the [ASP.NET Core security documentation](https://docs.microsoft.com/aspnet/core/security/). ### Notes diff --git a/src/Security/samples/Identity.ExternalClaims/README.md b/src/Security/samples/Identity.ExternalClaims/README.md index 7a9141075d..70205c0367 100644 --- a/src/Security/samples/Identity.ExternalClaims/README.md +++ b/src/Security/samples/Identity.ExternalClaims/README.md @@ -4,7 +4,7 @@ AuthSamples.Identity.ExternalClaims Sample demonstrating copying over static and dynamic external claims from Google authentication during login: Steps: -1. Configure a google OAuth2 project. See https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?tabs=aspnetcore2x for basic setup using google logins. +1. Configure a google OAuth2 project. See https://docs.microsoft.com/aspnet/core/security/authentication/social/google-logins for basic setup using google logins. 2. Update Startup.cs AddGoogle()'s options with ClientId and ClientSecret for your google app. 3. Run the app and click on the MyClaims tab, this should trigger a redirect to login. 4. Login via the Google button, this should redirect you to google. diff --git a/src/Servers/HttpSys/src/FeatureContext.cs b/src/Servers/HttpSys/src/FeatureContext.cs index a689230ab8..d2e2626874 100644 --- a/src/Servers/HttpSys/src/FeatureContext.cs +++ b/src/Servers/HttpSys/src/FeatureContext.cs @@ -174,23 +174,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys { if (IsNotInitialized(Fields.Protocol)) { - var protocol = Request.ProtocolVersion; - if (protocol == Constants.V2) - { - _httpProtocolVersion = "HTTP/2"; - } - else if (protocol == Constants.V1_1) - { - _httpProtocolVersion = "HTTP/1.1"; - } - else if (protocol == Constants.V1_0) - { - _httpProtocolVersion = "HTTP/1.0"; - } - else - { - _httpProtocolVersion = "HTTP/" + protocol.ToString(2); - } + _httpProtocolVersion = Request.ProtocolVersion.GetHttpProtocolVersion(); SetInitialized(Fields.Protocol); } return _httpProtocolVersion; diff --git a/src/Servers/HttpSys/src/MessagePump.cs b/src/Servers/HttpSys/src/MessagePump.cs index 9917e69872..4747a6ab5d 100644 --- a/src/Servers/HttpSys/src/MessagePump.cs +++ b/src/Servers/HttpSys/src/MessagePump.cs @@ -2,16 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Diagnostics.Contracts; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.AspNetCore.HttpSys.Internal; namespace Microsoft.AspNetCore.Server.HttpSys { @@ -74,6 +76,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys } var hostingUrlsPresent = _serverAddresses.Addresses.Count > 0; + var serverAddressCopy = _serverAddresses.Addresses.ToList(); + _serverAddresses.Addresses.Clear(); if (_serverAddresses.PreferHostingUrls && hostingUrlsPresent) { @@ -85,10 +89,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys Listener.Options.UrlPrefixes.Clear(); } - foreach (var value in _serverAddresses.Addresses) - { - Listener.Options.UrlPrefixes.Add(value); - } + UpdateUrlPrefixes(serverAddressCopy); } else if (_options.UrlPrefixes.Count > 0) { @@ -100,23 +101,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys _serverAddresses.Addresses.Clear(); } - foreach (var prefix in _options.UrlPrefixes) - { - _serverAddresses.Addresses.Add(prefix.FullPrefix); - } } else if (hostingUrlsPresent) { - foreach (var value in _serverAddresses.Addresses) - { - Listener.Options.UrlPrefixes.Add(value); - } + UpdateUrlPrefixes(serverAddressCopy); } else if (Listener.RequestQueue.Created) { LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default."); - _serverAddresses.Addresses.Add(Constants.DefaultServerAddress); Listener.Options.UrlPrefixes.Add(Constants.DefaultServerAddress); } // else // Attaching to an existing queue, don't add a default. @@ -130,6 +123,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys Listener.Start(); + // Update server addresses after we start listening as port 0 + // needs to be selected at the point of binding. + foreach (var prefix in _options.UrlPrefixes) + { + _serverAddresses.Addresses.Add(prefix.FullPrefix); + } + ActivateRequestProcessingLimits(); return Task.CompletedTask; @@ -143,6 +143,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } + private void UpdateUrlPrefixes(IList<string> serverAddressCopy) + { + foreach (var value in serverAddressCopy) + { + Listener.Options.UrlPrefixes.Add(value); + } + } + // The message pump. // When we start listening for the next request on one thread, we may need to be sure that the // completion continues on another thread as to not block the current request processing. diff --git a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs index 8f33c7b678..87a5012641 100644 --- a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs +++ b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -73,7 +73,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys { LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix); CheckDisposed(); - var statusCode = HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0); if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index bf663da134..496503e8ca 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -290,7 +290,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys Protocol = handshake.Protocol; // The OS considers client and server TLS as different enum values. SslProtocols choose to combine those for some reason. // We need to fill in the client bits so the enum shows the expected protocol. - // https://docs.microsoft.com/en-us/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo + // https://docs.microsoft.com/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo // Compare to https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,8905d1bf17729de3 #pragma warning disable CS0618 // Type or member is obsolete if ((Protocol & SslProtocols.Ssl2) != 0) diff --git a/src/Servers/HttpSys/src/UrlPrefixCollection.cs b/src/Servers/HttpSys/src/UrlPrefixCollection.cs index e38dd9a5d2..e29d1737d8 100644 --- a/src/Servers/HttpSys/src/UrlPrefixCollection.cs +++ b/src/Servers/HttpSys/src/UrlPrefixCollection.cs @@ -17,6 +17,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys private UrlGroup _urlGroup; private int _nextId = 1; + // Valid port range of 5000 - 48000. + private const int BasePort = 5000; + private const int MaxPortIndex = 43000; + private const int MaxRetries = 1000; + private static int NextPortIndex; + internal UrlPrefixCollection() { } @@ -166,10 +172,55 @@ namespace Microsoft.AspNetCore.Server.HttpSys { _urlGroup = urlGroup; // go through the uri list and register for each one of them - foreach (var pair in _prefixes) + // Call ToList to avoid modification when enumerating. + foreach (var pair in _prefixes.ToList()) { - // We'll get this index back on each request and use it to look up the prefix to calculate PathBase. - _urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key); + var urlPrefix = pair.Value; + if (urlPrefix.PortValue == 0) + { + if (urlPrefix.IsHttps) + { + throw new InvalidOperationException("Cannot bind to port 0 with https."); + } + + FindHttpPortUnsynchronized(pair.Key, urlPrefix); + } + else + { + // We'll get this index back on each request and use it to look up the prefix to calculate PathBase. + _urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key); + } + } + } + } + + private void FindHttpPortUnsynchronized(int key, UrlPrefix urlPrefix) + { + for (var index = 0; index < MaxRetries; index++) + { + try + { + // Bit of complicated math to always try 3000 ports, starting from NextPortIndex + 5000, + // circling back around if we go above 8000 back to 5000, and so on. + var port = ((index + NextPortIndex) % MaxPortIndex) + BasePort; + + Debug.Assert(port >= 5000 || port < 8000); + + var newPrefix = UrlPrefix.Create(urlPrefix.Scheme, urlPrefix.Host, port, urlPrefix.Path); + _urlGroup.RegisterPrefix(newPrefix.FullPrefix, key); + _prefixes[key] = newPrefix; + + NextPortIndex += index + 1; + return; + } + catch (HttpSysException ex) + { + if ((ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ACCESS_DENIED + && ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SHARING_VIOLATION + && ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS) || index == MaxRetries - 1) + { + throw; + } } } } diff --git a/src/Servers/HttpSys/startvs.cmd b/src/Servers/HttpSys/startvs.cmd new file mode 100644 index 0000000000..94b00042d2 --- /dev/null +++ b/src/Servers/HttpSys/startvs.cmd @@ -0,0 +1,3 @@ +@ECHO OFF + +%~dp0..\..\..\startvs.cmd %~dp0HttpSysServer.sln diff --git a/src/Servers/HttpSys/test/FunctionalTests/MessagePumpTests.cs b/src/Servers/HttpSys/test/FunctionalTests/MessagePumpTests.cs index 250ff9c9be..f9b4d81209 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/MessagePumpTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/MessagePumpTests.cs @@ -114,7 +114,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys { server.StartAsync(new DummyApplication(), CancellationToken.None).Wait(); - Assert.Equal(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single()); + // Trailing slash is added when put in UrlPrefix. + Assert.StartsWith(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single()); } } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs index 8ba9e7b39a..a347ce427f 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; @@ -21,11 +22,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys { // When tests projects are run in parallel, overlapping port ranges can cause a race condition when looking for free // ports during dynamic port allocation. - private const int BasePort = 5001; - private const int MaxPort = 8000; private const int BaseHttpsPort = 44300; private const int MaxHttpsPort = 44399; - private static int NextPort = BasePort; private static int NextHttpsPort = BaseHttpsPort; private static object PortLock = new object(); internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15); @@ -84,39 +82,26 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal static IWebHost CreateDynamicHost(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app) { - lock (PortLock) - { - while (NextPort < MaxPort) + var prefix = UrlPrefix.Create("http", "localhost", "0", basePath); + + var builder = new WebHostBuilder() + .UseHttpSys(options => { - var port = NextPort++; - var prefix = UrlPrefix.Create("http", "localhost", port, basePath); - root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port; - baseAddress = prefix.ToString(); + options.UrlPrefixes.Add(prefix); + configureOptions(options); + }) + .Configure(appBuilder => appBuilder.Run(app)); - var builder = new WebHostBuilder() - .UseHttpSys(options => - { - options.UrlPrefixes.Add(prefix); - configureOptions(options); - }) - .Configure(appBuilder => appBuilder.Run(app)); + var host = builder.Build(); - var host = builder.Build(); + host.Start(); + var options = host.Services.GetRequiredService<IOptions<HttpSysOptions>>(); + prefix = options.Value.UrlPrefixes.First(); // Has new port + root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port; + baseAddress = prefix.ToString(); - try - { - host.Start(); - return host; - } - catch (HttpSysException) - { - } - - } - NextPort = BasePort; - } - throw new Exception("Failed to locate a free port."); + return host; } internal static MessagePump CreatePump() @@ -131,30 +116,17 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app) { - lock (PortLock) - { - while (NextPort < MaxPort) - { + var prefix = UrlPrefix.Create("http", "localhost", "0", basePath); - var port = NextPort++; - var prefix = UrlPrefix.Create("http", "localhost", port, basePath); - root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port; - baseAddress = prefix.ToString(); + var server = CreatePump(configureOptions); + server.Features.Get<IServerAddressesFeature>().Addresses.Add(prefix.ToString()); + server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait(); - var server = CreatePump(configureOptions); - server.Features.Get<IServerAddressesFeature>().Addresses.Add(baseAddress); - try - { - server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait(); - return server; - } - catch (HttpSysException) - { - } - } - NextPort = BasePort; - } - throw new Exception("Failed to locate a free port."); + prefix = server.Listener.Options.UrlPrefixes.First(); // Has new port + root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port; + baseAddress = prefix.ToString(); + + return server; } internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app) diff --git a/src/Servers/HttpSys/test/Tests/UrlPrefixTests.cs b/src/Servers/HttpSys/test/Tests/UrlPrefixTests.cs index 8614ac36db..20d0f0713f 100644 --- a/src/Servers/HttpSys/test/Tests/UrlPrefixTests.cs +++ b/src/Servers/HttpSys/test/Tests/UrlPrefixTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs index cab956dd0a..6635385e7f 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.Server.IIS.Core.IO; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; @@ -37,7 +38,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core // then the list of `implementedFeatures` in the generated code project MUST also be updated. private int _featureRevision; - private string _httpProtocolVersion = null; + private string _httpProtocolVersion; private X509Certificate2 _certificate; private List<KeyValuePair<Type, object>> MaybeExtra; @@ -86,30 +87,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core string IHttpRequestFeature.Protocol { - get - { - if (_httpProtocolVersion == null) - { - var protocol = HttpVersion; - if (protocol.Major == 1 && protocol.Minor == 1) - { - _httpProtocolVersion = "HTTP/1.1"; - } - else if (protocol.Major == 1 && protocol.Minor == 0) - { - _httpProtocolVersion = "HTTP/1.0"; - } - else - { - _httpProtocolVersion = "HTTP/" + protocol.ToString(2); - } - } - return _httpProtocolVersion; - } - set - { - _httpProtocolVersion = value; - } + get => _httpProtocolVersion ??= HttpVersion.GetHttpProtocolVersion(); + set => _httpProtocolVersion = value; } string IHttpRequestFeature.Scheme diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/BasicAuthTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/BasicAuthTests.cs index 6130cdf882..36520369a6 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/BasicAuthTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/BasicAuthTests.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithApplicationTypes(ApplicationType.Portable) .WithAllHostingModels(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs index ccc4ea6458..6d39a56921 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes() .WithAllHostingModels(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/CommonStartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/CommonStartupTests.cs index 82df45fdbf..38f4dae818 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/CommonStartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/CommonStartupTests.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes() .WithAllHostingModels(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/HttpsTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/HttpsTests.cs index 9a931f7d52..8e3251570b 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/HttpsTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/HttpsTests.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes() .WithAllHostingModels(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs index 5445bd24cc..d3aecfc29c 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs @@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes() .WithAncmV2InProcess(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/LogFileTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/LogFileTests.cs index 699bd8b3ac..ffde811027 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/LogFileTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/LogFileTests.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes() .WithAllHostingModels(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs index ddc981d7af..df160cb44c 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.OutOfProcess public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithApplicationTypes(ApplicationType.Portable); public static IEnumerable<object[]> InvalidTestVariants diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs index deaf45f95a..2054011422 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.OutOfProcess public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllApplicationTypes(); [ConditionalTheory] diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedSitesFixture.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedSitesFixture.cs index a0fd00f869..70a7606e31 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedSitesFixture.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedSitesFixture.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests RuntimeFlavor = RuntimeFlavor.CoreClr, RuntimeArchitecture = RuntimeArchitecture.x64, HostingModel = hostingModel, - TargetFramework = Tfm.NetCoreApp31 + TargetFramework = Tfm.NetCoreApp50 }); } diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs index 645e595cbf..29d6f9c80c 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs @@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests { RuntimeArchitecture = RuntimeArchitecture.x64, RuntimeFlavor = RuntimeFlavor.CoreClr, - TargetFramework = Tfm.NetCoreApp31, + TargetFramework = Tfm.NetCoreApp50, HostingModel = HostingModel.InProcess, PublishApplicationBeforeDeployment = true, ApplicationPublisher = new PublishedApplicationPublisher(Helpers.GetInProcessTestSitesName()), diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/WindowsAuthTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/WindowsAuthTests.cs index f9716c2a26..e22f96dadc 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/WindowsAuthTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/WindowsAuthTests.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithApplicationTypes(ApplicationType.Portable) .WithAllHostingModels(); diff --git a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs index 17f557a338..c63c777c80 100644 --- a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs +++ b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp31); + .WithTfms(Tfm.NetCoreApp50); [ConditionalTheory] [RequiresIIS(IISCapability.WindowsAuthentication)] diff --git a/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs b/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs index 929a408778..16f7ab0fce 100644 --- a/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs +++ b/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs @@ -139,6 +139,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core BadHttpRequestException ex; switch (reason) { + case RequestRejectionReason.TlsOverHttpError: + ex = new BadHttpRequestException(CoreStrings.HttpParserTlsOverHttpError, StatusCodes.Status400BadRequest, reason); + break; case RequestRejectionReason.InvalidRequestLine: ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(detail), StatusCodes.Status400BadRequest, reason); break; diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 0f49dedc81..20af4f4a9c 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -617,4 +617,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l <data name="Http2TellClientToCalmDown" xml:space="preserve"> <value>A new stream was refused because this connection has too many streams that haven't finished processing. This may happen if many streams are aborted but not yet cleaned up.</value> </data> -</root> + <data name="HttpParserTlsOverHttpError" xml:space="preserve"> + <value>Detected a TLS handshake to an endpoint that does not have TLS enabled.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index 2fe5dfdb36..ce63ec989f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private const byte ByteTab = (byte)'\t'; private const byte ByteQuestionMark = (byte)'?'; private const byte BytePercentage = (byte)'%'; + private const int MinTlsRequestSize = 1; // We need at least 1 byte to check for a proper TLS request line public unsafe bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined) { @@ -415,9 +416,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return new Span<byte>(data, methodLength); } + private unsafe bool IsTlsHandshake(byte* data, int length) + { + const byte SslRecordTypeHandshake = (byte)0x16; + + // Make sure we can check at least for the existence of a TLS handshake - we check the first byte + // See https://serializethoughts.com/2014/07/27/dissecting-tls-client-hello-message/ + + return (length >= MinTlsRequestSize && data[0] == SslRecordTypeHandshake); + } + [StackTraceHidden] private unsafe void RejectRequestLine(byte* requestLine, int length) - => throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length); + { + // Check for incoming TLS handshake over HTTP + if (IsTlsHandshake(requestLine, length)) + { + throw GetInvalidRequestException(RequestRejectionReason.TlsOverHttpError, requestLine, length); + } + else + { + throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length); + } + } [StackTraceHidden] private unsafe void RejectRequestHeader(byte* headerLine, int length) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs b/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs index fce21b6210..23dc6c67c6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs @@ -1,10 +1,11 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal enum RequestRejectionReason { + TlsOverHttpError, UnrecognizedHTTPVersion, InvalidRequestLine, InvalidRequestHeader, diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 3ccdccef69..10756c0e80 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -204,27 +204,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 while (_isClosed == 0) { var result = await Input.ReadAsync(); - var readableBuffer = result.Buffer; - var consumed = readableBuffer.Start; - var examined = readableBuffer.Start; + var buffer = result.Buffer; // Call UpdateCompletedStreams() prior to frame processing in order to remove any streams that have exceded their drain timeouts. UpdateCompletedStreams(); try { - if (!readableBuffer.IsEmpty) + while (Http2FrameReader.TryReadFrame(ref buffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload)) { - if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload)) - { - Log.Http2FrameReceived(ConnectionId, _incomingFrame); - consumed = examined = framePayload.End; - await ProcessFrameAsync(application, framePayload); - } - else - { - examined = readableBuffer.End; - } + Log.Http2FrameReceived(ConnectionId, _incomingFrame); + await ProcessFrameAsync(application, framePayload); } if (result.IsCompleted) @@ -242,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } finally { - Input.AdvanceTo(consumed, examined); + Input.AdvanceTo(buffer.Start, buffer.End); UpdateConnectionState(); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameReader.cs index 8437ad334a..ed4db88f0e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameReader.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameReader.cs @@ -31,16 +31,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value. - public static bool ReadFrame(in ReadOnlySequence<byte> readableBuffer, Http2Frame frame, uint maxFrameSize, out ReadOnlySequence<byte> framePayload) + public static bool TryReadFrame(ref ReadOnlySequence<byte> buffer, Http2Frame frame, uint maxFrameSize, out ReadOnlySequence<byte> framePayload) { framePayload = ReadOnlySequence<byte>.Empty; - if (readableBuffer.Length < HeaderLength) + if (buffer.Length < HeaderLength) { return false; } - var headerSlice = readableBuffer.Slice(0, HeaderLength); + var headerSlice = buffer.Slice(0, HeaderLength); var header = headerSlice.ToSpan(); var payloadLength = (int)Bitshifter.ReadUInt24BigEndian(header); @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // Make sure the whole frame is buffered var frameLength = HeaderLength + payloadLength; - if (readableBuffer.Length < frameLength) + if (buffer.Length < frameLength) { return false; } @@ -61,10 +61,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 frame.Flags = header[FlagsOffset]; frame.StreamId = (int)Bitshifter.ReadUInt31BigEndian(header.Slice(StreamIdOffset)); - var extendedHeaderLength = ReadExtendedFields(frame, readableBuffer); + var extendedHeaderLength = ReadExtendedFields(frame, buffer); // The remaining payload minus the extra fields - framePayload = readableBuffer.Slice(HeaderLength + extendedHeaderLength, payloadLength - extendedHeaderLength); + framePayload = buffer.Slice(HeaderLength + extendedHeaderLength, payloadLength - extendedHeaderLength); + buffer = buffer.Slice(framePayload.End); return true; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 7555b9f223..1177504aa6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence<byte> data, bool endStream) + public ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence<byte> data, bool endStream, bool forceFlush) { // The Length property of a ReadOnlySequence can be expensive, so we cache the value. var dataLength = data.Length; @@ -261,7 +261,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // This cast is safe since if dataLength would overflow an int, it's guaranteed to be greater than the available flow control window. flowControl.Advance((int)dataLength); WriteDataUnsynchronized(streamId, data, dataLength, endStream); - return TimeFlushUnsynchronizedAsync(); + + if (forceFlush) + { + return TimeFlushUnsynchronizedAsync(); + } + + return default; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs index 18adcc1a82..3bd9794e47 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -380,7 +380,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // Write any remaining content then write trailers if (readResult.Buffer.Length > 0) { - flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false); + // Only flush if required (i.e. content length exceeds flow control availability) + // Writing remaining content without flushing allows content and trailers to be sent in the same packet + await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false, forceFlush: false); } _stream.ResponseTrailers.SetReadOnly(); @@ -404,7 +406,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { _stream.DecrementActiveClientStreamCount(); } - flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream); + flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream, forceFlush: true); } _pipeReader.AdvanceTo(readResult.Buffer.End); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs index fdd8ac7367..a9bf8d9424 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs @@ -59,20 +59,16 @@ namespace System.IO.Pipelines AvailableMemory = arrayPoolBuffer; } - public void SetUnownedMemory(Memory<byte> memory) - { - AvailableMemory = memory; - } - public void ResetMemory() { if (_memoryOwner is IMemoryOwner<byte> owner) { owner.Dispose(); } - else if (_memoryOwner is byte[] array) + else { - ArrayPool<byte>.Shared.Return(array); + byte[] poolArray = (byte[])_memoryOwner; + ArrayPool<byte>.Shared.Return(poolArray); } // Order of below field clears is significant as it clears in a sequential order diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs index 3ab051c8c4..4e09b0a8a4 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs @@ -341,8 +341,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeW } else { - // We can't use the pool so allocate an array - newSegment.SetUnownedMemory(new byte[sizeHint]); + // We can't use the recommended pool so use the ArrayPool + newSegment.SetOwnedMemory(ArrayPool<byte>.Shared.Rent(sizeHint)); } _tailMemory = newSegment.AvailableMemory; diff --git a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs index 79366d276e..48d9edd99e 100644 --- a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs +++ b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs @@ -77,6 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win8, WindowsVersions.Win81, WindowsVersions.Win2008R2, SkipReason = "UnixDomainSocketEndPoint is not supported on older versions of Windows")] + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/14382", Queues = "Windows.10.Amd64.Open")] [ConditionalFact] public void ParseAddressUnixPipe() { diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs index 7ce8587743..82d69d8b4d 100644 --- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -395,6 +395,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [Fact] + public void ParseRequestLineTlsOverHttp() + { + var parser = CreateParser(_nullTrace); + var buffer = ReadOnlySequenceFactory.CreateSegments(new byte[] { 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0xfc, 0x03, 0x03, 0x03, 0xca, 0xe0, 0xfd, 0x0a }); + + var requestHandler = new RequestHandler(); + + var badHttpRequestException = Assert.Throws<BadHttpRequestException>(() => + { + parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined); + }); + + Assert.Equal(badHttpRequestException.StatusCode, StatusCodes.Status400BadRequest); + Assert.Equal(RequestRejectionReason.TlsOverHttpError, badHttpRequestException.Reason); + } + + [Fact] public void ParseHeadersWithGratuitouslySplitBuffers() { var parser = CreateParser(_nullTrace); diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs index ccdb774674..600d674d98 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -62,6 +62,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets throw new InvalidOperationException(SocketsStrings.TransportAlreadyBound); } + // Check if EndPoint is a FileHandleEndpoint before attempting to access EndPoint.AddressFamily + // since that will throw an NotImplementedException. + if (EndPoint is FileHandleEndPoint) + { + throw new NotSupportedException(SocketsStrings.FileHandleEndPointNotSupported); + } + Socket listenSocket; // Unix domain sockets are unspecified diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketsStrings.resx b/src/Servers/Kestrel/Transport.Sockets/src/SocketsStrings.resx index 52b26c66bc..5f1475a1cf 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketsStrings.resx +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketsStrings.resx @@ -117,10 +117,13 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> + <data name="FileHandleEndPointNotSupported" xml:space="preserve"> + <value>The Socket transport does not support binding to file handles. Consider using the libuv transport instead.</value> + </data> <data name="OnlyIPEndPointsSupported" xml:space="preserve"> <value>Only ListenType.IPEndPoint is supported by the Socket Transport. https://go.microsoft.com/fwlink/?linkid=874850</value> </data> <data name="TransportAlreadyBound" xml:space="preserve"> <value>Transport is already bound.</value> </data> -</root>
\ No newline at end of file +</root> diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 7ddb4deb26..47e3288be5 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 await stopTask.DefaultTimeout(); } - Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("Request finished in")); + Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("Request finished ")); Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("is closing.")); Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("is closed. The last processed stream ID was 1.")); } diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs index d3fa68a1bd..14cea7f48a 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; @@ -755,6 +756,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests }; testContext.InitializeHeartbeat(); + var dateHeaderValueManager = new DateHeaderValueManager(); + dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); + testContext.DateHeaderValueManager = dateHeaderValueManager; var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); @@ -781,16 +785,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await connection.Send( "GET / HTTP/1.1", "Host:", - "Connection: close", "", ""); - var minTotalOutputSize = chunkCount * chunkSize; + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}"); // Make sure consuming a single chunk exceeds the 2 second timeout. var targetBytesPerSecond = chunkSize / 4; - await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond); + + // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless + // the response header writing logic or response body chunking logic itself changes. + await AssertBytesReceivedAtTargetRate(connection.Stream, expectedBytes: 33_553_537, targetBytesPerSecond); await appFuncCompleted.Task.DefaultTimeout(); + + connection.ShutdownSend(); + await connection.WaitForConnectionClose(); } await server.StopAsync(); } @@ -800,11 +811,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.False(requestAborted); } - private bool ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeadersRetryPredicate(Exception e) - => e is IOException && e.Message.Contains("Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request"); - [Fact] - [Flaky("https://github.com/dotnet/corefx/issues/30691", FlakyOn.AzP.Windows)] [CollectDump] public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders() { @@ -829,6 +836,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests }; testContext.InitializeHeartbeat(); + var dateHeaderValueManager = new DateHeaderValueManager(); + dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); + testContext.DateHeaderValueManager = dateHeaderValueManager; var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); @@ -858,6 +868,86 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests ""); } + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}"); + + var minResponseSize = headerSize * headerCount; + var minTotalOutputSize = requestCount * minResponseSize; + + // Make sure consuming a single set of response headers exceeds the 2 second timeout. + var targetBytesPerSecond = minResponseSize / 4; + + // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless + // the response header writing logic itself changes. + await AssertBytesReceivedAtTargetRate(connection.Stream, expectedBytes: 268_439_596, targetBytesPerSecond); + connection.ShutdownSend(); + await connection.WaitForConnectionClose(); + } + + await server.StopAsync(); + } + + mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>()), Times.Never()); + mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny<string>()), Times.Once()); + Assert.False(requestAborted); + } + + [Fact] + [Flaky("https://github.com/aspnet/AspNetCore/issues/13219", FlakyOn.AzP.Linux, FlakyOn.Helix.All)] + public async Task ClientCanReceiveFullConnectionCloseResponseWithoutErrorAtALowDataRate() + { + var chunkSize = 64 * 128 * 1024; + var chunkCount = 4; + var chunkData = new byte[chunkSize]; + + var requestAborted = false; + var appFuncCompleted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + var mockKestrelTrace = new Mock<IKestrelTrace>(); + + var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) + { + ServerOptions = + { + Limits = + { + MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) + } + } + }; + + testContext.InitializeHeartbeat(); + var dateHeaderValueManager = new DateHeaderValueManager(); + dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); + testContext.DateHeaderValueManager = dateHeaderValueManager; + + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + + async Task App(HttpContext context) + { + context.RequestAborted.Register(() => + { + requestAborted = true; + }); + + for (var i = 0; i < chunkCount; i++) + { + await context.Response.BodyWriter.WriteAsync(new Memory<byte>(chunkData, 0, chunkData.Length), context.RequestAborted); + } + + appFuncCompleted.SetResult(null); + } + + using (var server = new TestServer(App, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { // Close the connection with the last request so AssertStreamCompleted actually completes. await connection.Send( "GET / HTTP/1.1", @@ -866,12 +956,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - var responseSize = headerSize * headerCount; - var minTotalOutputSize = requestCount * responseSize; + await connection.Receive( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}"); + + // Make sure consuming a single chunk exceeds the 2 second timeout. + var targetBytesPerSecond = chunkSize / 4; - // Make sure consuming a single set of response headers exceeds the 2 second timeout. - var targetBytesPerSecond = responseSize / 4; - await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond); + // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless + // the response header writing logic or response body chunking logic itself changes. + await AssertStreamCompletedAtTargetRate(connection.Stream, expectedBytes: 33_553_556, targetBytesPerSecond); + await appFuncCompleted.Task.DefaultTimeout(); } await server.StopAsync(); } @@ -908,7 +1004,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully."); } - private async Task AssertStreamCompleted(Stream stream, long minimumBytes, int targetBytesPerSecond) + private async Task AssertBytesReceivedAtTargetRate(Stream stream, int expectedBytes, int targetBytesPerSecond) + { + var receiveBuffer = new byte[64 * 1024]; + var totalReceived = 0; + var startTime = DateTimeOffset.UtcNow; + + do + { + var received = await stream.ReadAsync(receiveBuffer, 0, Math.Min(receiveBuffer.Length, expectedBytes - totalReceived)); + + Assert.NotEqual(0, received); + + totalReceived += received; + + var expectedTimeElapsed = TimeSpan.FromSeconds(totalReceived / targetBytesPerSecond); + var timeElapsed = DateTimeOffset.UtcNow - startTime; + if (timeElapsed < expectedTimeElapsed) + { + await Task.Delay(expectedTimeElapsed - timeElapsed); + } + } while (totalReceived < expectedBytes); + } + + private async Task AssertStreamCompletedAtTargetRate(Stream stream, long expectedBytes, int targetBytesPerSecond) { var receiveBuffer = new byte[64 * 1024]; var received = 0; @@ -928,7 +1047,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } while (received > 0); - Assert.True(totalReceived >= minimumBytes, $"{nameof(AssertStreamCompleted)} Stream aborted prematurely."); + Assert.Equal(expectedBytes, totalReceived); } public static TheoryData<string, StringValues, string> NullHeaderData diff --git a/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs b/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs index d34ced5292..09cda9ad51 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs @@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests #else [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win8, WindowsVersions.Win81, WindowsVersions.Win2008R2, SkipReason = "UnixDomainSocketEndPoint is not supported on older versions of Windows")] #endif + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/14382", Queues = "Windows.10.Amd64.Open")] [ConditionalFact] [CollectDump] public async Task TestUnixDomainSocket() diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 4937000db9..00f8f3b829 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -2045,6 +2045,127 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [Fact] + public async Task ResponseTrailers_WithLargeUnflushedData_DataExceedsFlowControlAvailableAndNotSentWithTrailers() + { + const int windowSize = (int)Http2PeerSettings.DefaultMaxFrameSize; + _clientSettings.InitialWindowSize = windowSize; + + var headers = new[] + { + new KeyValuePair<string, string>(HeaderNames.Method, "GET"), + new KeyValuePair<string, string>(HeaderNames.Path, "/"), + new KeyValuePair<string, string>(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + await context.Response.StartAsync(); + + // Body exceeds flow control available and requires the client to allow more + // data via updating the window + context.Response.BodyWriter.GetMemory(windowSize + 1); + context.Response.BodyWriter.Advance(windowSize + 1); + + context.Response.AppendTrailer("CustomName", "Custom Value"); + }).DefaultTimeout(); + + await StartStreamAsync(1, headers, endStream: true).DefaultTimeout(); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1).DefaultTimeout(); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 16384, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1).DefaultTimeout(); + + var dataTask = ExpectAsync(Http2FrameType.DATA, + withLength: 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1).DefaultTimeout(); + + // Reading final frame of data requires window update + // Verify this data task is waiting on window update + Assert.False(dataTask.IsCompletedSuccessfully); + + await SendWindowUpdateAsync(1, 1); + + await dataTask; + + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 25, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1).DefaultTimeout(); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout(); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + + _decodedHeaders.Clear(); + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Single(_decodedHeaders); + Assert.Equal("Custom Value", _decodedHeaders["CustomName"]); + } + + [Fact] + public async Task ResponseTrailers_WithUnflushedData_DataSentWithTrailers() + { + var headers = new[] + { + new KeyValuePair<string, string>(HeaderNames.Method, "GET"), + new KeyValuePair<string, string>(HeaderNames.Path, "/"), + new KeyValuePair<string, string>(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + await context.Response.StartAsync(); + + var s = context.Response.BodyWriter.GetMemory(1); + s.Span[0] = byte.MaxValue; + context.Response.BodyWriter.Advance(1); + + context.Response.AppendTrailer("CustomName", "Custom Value"); + }); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 25, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + + _decodedHeaders.Clear(); + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this); + + Assert.Single(_decodedHeaders); + Assert.Equal("Custom Value", _decodedHeaders["CustomName"]); + } + + [Fact] public async Task ApplicationException_BeforeFirstWrite_Sends500() { var headers = new[] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 137ea743b7..24e4e59228 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -1112,12 +1112,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = result.Buffer; var consumed = buffer.Start; var examined = buffer.Start; + var copyBuffer = buffer; try { Assert.True(buffer.Length > 0); - if (Http2FrameReader.ReadFrame(buffer, frame, maxFrameSize, out var framePayload)) + if (Http2FrameReader.TryReadFrame(ref buffer, frame, maxFrameSize, out var framePayload)) { consumed = examined = framePayload.End; frame.Payload = framePayload.ToArray(); @@ -1135,7 +1136,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } finally { - _bytesReceived += buffer.Slice(buffer.Start, consumed).Length; + _bytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length; _pair.Application.Input.AdvanceTo(consumed, examined); } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs index f52e83eb62..1c78af53bf 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs @@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2 try { - if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out var framePayload)) + if (Http2FrameReader.TryReadFrame(ref buffer, frame, 16_384, out var framePayload)) { consumed = examined = framePayload.End; return frame; diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportFactoryTests.cs b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportFactoryTests.cs new file mode 100644 index 0000000000..2ff92f497f --- /dev/null +++ b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportFactoryTests.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Sockets.BindTests +{ + public class SocketTransportFactoryTests + { + [Fact] + public async Task ThrowsNotSupportedExceptionWhenBindingToFileHandleEndPoint() + { + var socketTransportFactory = new SocketTransportFactory(Options.Create(new SocketTransportOptions()), Mock.Of<ILoggerFactory>()); + await Assert.ThrowsAsync<NotSupportedException>(async () => await socketTransportFactory.BindAsync(new FileHandleEndPoint(0, FileHandleType.Auto))); + } + } +} + diff --git a/src/Servers/test/FunctionalTests/HelloWorldTest.cs b/src/Servers/test/FunctionalTests/HelloWorldTest.cs index e047288908..cf20b9d617 100644 --- a/src/Servers/test/FunctionalTests/HelloWorldTest.cs +++ b/src/Servers/test/FunctionalTests/HelloWorldTest.cs @@ -21,7 +21,7 @@ namespace ServerComparison.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.Nginx, ServerType.HttpSys) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithApplicationTypes(ApplicationType.Portable) .WithAllHostingModels() .WithAllArchitectures(); diff --git a/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs b/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs index 330586eafa..75ef46207a 100644 --- a/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs +++ b/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs @@ -22,7 +22,7 @@ namespace ServerComparison.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.HttpSys, ServerType.Kestrel) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllHostingModels(); [ConditionalTheory] diff --git a/src/Servers/test/FunctionalTests/ResponseCompressionTests.cs b/src/Servers/test/FunctionalTests/ResponseCompressionTests.cs index 4f4af0b3e0..e441343521 100644 --- a/src/Servers/test/FunctionalTests/ResponseCompressionTests.cs +++ b/src/Servers/test/FunctionalTests/ResponseCompressionTests.cs @@ -33,7 +33,7 @@ namespace ServerComparison.FunctionalTests public static TestMatrix NoCompressionTestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.Nginx, ServerType.HttpSys) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllHostingModels(); [ConditionalTheory] @@ -45,7 +45,7 @@ namespace ServerComparison.FunctionalTests public static TestMatrix HostCompressionTestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.Nginx) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllHostingModels(); [ConditionalTheory] @@ -57,7 +57,7 @@ namespace ServerComparison.FunctionalTests public static TestMatrix AppCompressionTestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.HttpSys) // No pass-through compression for nginx - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllHostingModels(); [ConditionalTheory] @@ -69,7 +69,7 @@ namespace ServerComparison.FunctionalTests public static TestMatrix HostAndAppCompressionTestVariants => TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.Nginx, ServerType.HttpSys) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllHostingModels(); [ConditionalTheory] diff --git a/src/Servers/test/FunctionalTests/ResponseTests.cs b/src/Servers/test/FunctionalTests/ResponseTests.cs index 169132f30b..904ec52b7a 100644 --- a/src/Servers/test/FunctionalTests/ResponseTests.cs +++ b/src/Servers/test/FunctionalTests/ResponseTests.cs @@ -26,7 +26,7 @@ namespace ServerComparison.FunctionalTests public static TestMatrix TestVariants => TestMatrix.ForServers(/* ServerType.IISExpress, https://github.com/aspnet/AspNetCore/issues/6168, */ ServerType.Kestrel, ServerType.Nginx, ServerType.HttpSys) - .WithTfms(Tfm.NetCoreApp31) + .WithTfms(Tfm.NetCoreApp50) .WithAllHostingModels(); [ConditionalTheory] @@ -52,7 +52,7 @@ namespace ServerComparison.FunctionalTests public static TestMatrix SelfhostTestVariants => TestMatrix.ForServers(ServerType.Kestrel, ServerType.HttpSys) - .WithTfms(Tfm.NetCoreApp31); + .WithTfms(Tfm.NetCoreApp50); // Connection Close tests do not work through reverse proxies [ConditionalTheory] diff --git a/src/Shared/E2ETesting/BrowserFixture.cs b/src/Shared/E2ETesting/BrowserFixture.cs index 7808176318..8b3239c5df 100644 --- a/src/Shared/E2ETesting/BrowserFixture.cs +++ b/src/Shared/E2ETesting/BrowserFixture.cs @@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.E2ETesting // To prevent this we let the client attempt several times to connect to the server, increasing // the max allowed timeout for a command on each attempt linearly. // This can also be caused if many tests are running concurrently, we might want to manage - // chrome and chromedriver instances more aggresively if we have to. + // chrome and chromedriver instances more aggressively if we have to. // Additionally, if we think the selenium server has become irresponsive, we could spin up // replace the current selenium server instance and let a new instance take over for the // remaining tests. diff --git a/src/Shared/E2ETesting/SeleniumStandaloneServer.cs b/src/Shared/E2ETesting/SeleniumStandaloneServer.cs index d62b3ffb00..91f80afb2b 100644 --- a/src/Shared/E2ETesting/SeleniumStandaloneServer.cs +++ b/src/Shared/E2ETesting/SeleniumStandaloneServer.cs @@ -191,7 +191,7 @@ Captured output lines: private static Process StartSentinelProcess(Process process, string sentinelFile, int timeout) { - // This sentinel process will start and will kill any roge selenium server that want' torn down + // This sentinel process will start and will kill any rouge selenium server that want' torn down // via normal means. var psi = new ProcessStartInfo { diff --git a/src/Shared/ErrorPage/GeneratePage.ps1 b/src/Shared/ErrorPage/GeneratePage.ps1 index 8bc0f2c07c..94e6746169 100644 --- a/src/Shared/ErrorPage/GeneratePage.ps1 +++ b/src/Shared/ErrorPage/GeneratePage.ps1 @@ -2,7 +2,7 @@ param( [Parameter(Mandatory = $true)][string]$ToolingRepoPath ) -$ToolPath = Join-Path $ToolingRepoPath "artifacts\bin\RazorPageGenerator\Debug\netcoreapp3.1\dotnet-razorpagegenerator.exe" +$ToolPath = Join-Path $ToolingRepoPath "artifacts\bin\RazorPageGenerator\Debug\netcoreapp5.0\dotnet-razorpagegenerator.exe" if (!(Test-Path $ToolPath)) { throw "Unable to find razor page generator tool at $ToolPath" diff --git a/src/Shared/HttpSys/Constants.cs b/src/Shared/HttpSys/Constants.cs index 6f861c239f..4d0576c477 100644 --- a/src/Shared/HttpSys/Constants.cs +++ b/src/Shared/HttpSys/Constants.cs @@ -15,8 +15,8 @@ namespace Microsoft.AspNetCore.HttpSys.Internal internal const string SchemeDelimiter = "://"; internal const string DefaultServerAddress = "http://localhost:5000"; - internal static Version V1_0 = new Version(1, 0); - internal static Version V1_1 = new Version(1, 1); - internal static Version V2 = new Version(2, 0); + internal static readonly Version V1_0 = new Version(1, 0); + internal static readonly Version V1_1 = new Version(1, 1); + internal static readonly Version V2 = new Version(2, 0); } } diff --git a/src/Shared/HttpSys/Extensions.cs b/src/Shared/HttpSys/Extensions.cs new file mode 100644 index 0000000000..c813991357 --- /dev/null +++ b/src/Shared/HttpSys/Extensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.HttpSys.Internal +{ + internal static class Extensions + { + public static string GetHttpProtocolVersion(this Version version) => version switch + { + { Major: 2, Minor: 0 } => "HTTP/2", + { Major: 1, Minor: 1 } => "HTTP/1.1", + { Major: 1, Minor: 0 } => "HTTP/1.0", + _ => "HTTP/" + version.ToString(2) + }; + } +} diff --git a/src/SignalR/README.md b/src/SignalR/README.md index 084b5fbf71..80a69f98e7 100644 --- a/src/SignalR/README.md +++ b/src/SignalR/README.md @@ -7,7 +7,7 @@ You can watch an introductory presentation here - [ASP.NET Core SignalR: Build 2 ## Documentation -Documentation for ASP.NET Core SignalR can be found in the [Real-time Apps](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-2.1) section of the ASP.NET Core Documentation site. +Documentation for ASP.NET Core SignalR can be found in the [Real-time Apps](https://docs.microsoft.com/aspnet/core/signalr/introduction) section of the ASP.NET Core Documentation site. ## TypeScript Version diff --git a/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs b/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs index a361a4ed03..0635cd850a 100644 --- a/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs +++ b/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.SignalR.Client { - public partial class HubConnection + public partial class HubConnection : System.IAsyncDisposable { public static readonly System.TimeSpan DefaultHandshakeTimeout; public static readonly System.TimeSpan DefaultKeepAliveInterval; @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Client public event System.Func<string, System.Threading.Tasks.Task> Reconnected { add { } remove { } } public event System.Func<System.Exception, System.Threading.Tasks.Task> Reconnecting { add { } remove { } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task DisposeAsync() { throw null; } + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task<object> InvokeCoreAsync(string methodName, System.Type returnType, object[] args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.IDisposable On(string methodName, System.Type[] parameterTypes, System.Func<object[], object, System.Threading.Tasks.Task> handler, object state) { throw null; } diff --git a/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.1.cs b/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.1.cs index a361a4ed03..0635cd850a 100644 --- a/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.1.cs +++ b/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.1.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.SignalR.Client { - public partial class HubConnection + public partial class HubConnection : System.IAsyncDisposable { public static readonly System.TimeSpan DefaultHandshakeTimeout; public static readonly System.TimeSpan DefaultKeepAliveInterval; @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Client public event System.Func<string, System.Threading.Tasks.Task> Reconnected { add { } remove { } } public event System.Func<System.Exception, System.Threading.Tasks.Task> Reconnecting { add { } remove { } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task DisposeAsync() { throw null; } + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task<object> InvokeCoreAsync(string methodName, System.Type returnType, object[] args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.IDisposable On(string methodName, System.Type[] parameterTypes, System.Func<object[], object, System.Threading.Tasks.Task> handler, object state) { throw null; } diff --git a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs index 5e04bd309a..576e69326f 100644 --- a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs +++ b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs @@ -22,7 +22,6 @@ using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.SignalR.Client { @@ -34,7 +33,7 @@ namespace Microsoft.AspNetCore.SignalR.Client /// Before hub methods can be invoked the connection must be started using <see cref="StartAsync"/>. /// Clean up a connection using <see cref="StopAsync"/> or <see cref="DisposeAsync"/>. /// </remarks> - public partial class HubConnection + public partial class HubConnection : IAsyncDisposable { public static readonly TimeSpan DefaultServerTimeout = TimeSpan.FromSeconds(30); // Server ping rate is 15 sec, this is 2 times that. public static readonly TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(15); @@ -292,8 +291,8 @@ namespace Microsoft.AspNetCore.SignalR.Client /// <summary> /// Disposes the <see cref="HubConnection"/>. /// </summary> - /// <returns>A <see cref="Task"/> that represents the asynchronous dispose.</returns> - public async Task DisposeAsync() + /// <returns>A <see cref="ValueTask"/> that represents the asynchronous dispose.</returns> + public async ValueTask DisposeAsync() { if (!_disposed) { @@ -504,8 +503,16 @@ namespace Microsoft.AspNetCore.SignalR.Client if (disposing) { - (_serviceProvider as IDisposable)?.Dispose(); + // Must set this before calling DisposeAsync because the service provider has a reference to the HubConnection and will try to dispose it again _disposed = true; + if (_serviceProvider is IAsyncDisposable asyncDispose) + { + await asyncDispose.DisposeAsync(); + } + else + { + (_serviceProvider as IDisposable)?.Dispose(); + } } } finally @@ -532,7 +539,7 @@ namespace Microsoft.AspNetCore.SignalR.Client /// </returns> public IAsyncEnumerable<TResult> StreamAsyncCore<TResult>(string methodName, object[] args, CancellationToken cancellationToken = default) { - var cts = cancellationToken.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : new CancellationTokenSource(); + var cts = cancellationToken.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default) : new CancellationTokenSource(); var stream = CastIAsyncEnumerable<TResult>(methodName, args, cts); var cancelableStream = AsyncEnumerableAdapters.MakeCancelableTypedAsyncEnumerable(stream, cts); return cancelableStream; diff --git a/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj b/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj index df54c48fec..e287d4c869 100644 --- a/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj +++ b/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj @@ -32,7 +32,7 @@ <Reference Include="Microsoft.Bcl.AsyncInterfaces" /> </ItemGroup> - <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.1'"> + <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'"> <!-- This dependency was replaced by Protocols.NewtonsoftJson between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <SuppressBaselineReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" /> </ItemGroup> diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs index 6dbd4e032e..5d40dcaf70 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs @@ -1479,6 +1479,118 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests } } + [Fact] + public async Task UserAgentIsSet() + { + using (StartServer<Startup>(out var server)) + { + var hubConnection = new HubConnectionBuilder() + .WithLoggerFactory(LoggerFactory) + .WithUrl(server.Url + "/default", HttpTransportType.LongPolling, options => + { + options.Headers["X-test"] = "42"; + options.Headers["X-42"] = "test"; + }) + .Build(); + try + { + await hubConnection.StartAsync().OrTimeout(); + var headerValues = await hubConnection.InvokeAsync<string[]>(nameof(TestHub.GetHeaderValues), new[] { "User-Agent" }).OrTimeout(); + Assert.NotNull(headerValues); + Assert.Single(headerValues); + + var userAgent = headerValues[0]; + + Assert.StartsWith("Microsoft SignalR/", userAgent); + + var majorVersion = typeof(HttpConnection).Assembly.GetName().Version.Major; + var minorVersion = typeof(HttpConnection).Assembly.GetName().Version.Minor; + + Assert.Contains($"{majorVersion}.{minorVersion}", userAgent); + + } + catch (Exception ex) + { + LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + } + } + } + + [Fact] + public async Task UserAgentCanBeCleared() + { + using (StartServer<Startup>(out var server)) + { + var hubConnection = new HubConnectionBuilder() + .WithLoggerFactory(LoggerFactory) + .WithUrl(server.Url + "/default", HttpTransportType.LongPolling, options => + { + options.Headers["User-Agent"] = ""; + }) + .Build(); + try + { + await hubConnection.StartAsync().OrTimeout(); + var headerValues = await hubConnection.InvokeAsync<string[]>(nameof(TestHub.GetHeaderValues), new[] { "User-Agent" }).OrTimeout(); + Assert.NotNull(headerValues); + Assert.Single(headerValues); + + var userAgent = headerValues[0]; + + Assert.Null(userAgent); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + } + } + } + + [Fact] + public async Task UserAgentCanBeSet() + { + using (StartServer<Startup>(out var server)) + { + var hubConnection = new HubConnectionBuilder() + .WithLoggerFactory(LoggerFactory) + .WithUrl(server.Url + "/default", HttpTransportType.LongPolling, options => + { + options.Headers["User-Agent"] = "User Value"; + }) + .Build(); + try + { + await hubConnection.StartAsync().OrTimeout(); + var headerValues = await hubConnection.InvokeAsync<string[]>(nameof(TestHub.GetHeaderValues), new[] { "User-Agent" }).OrTimeout(); + Assert.NotNull(headerValues); + Assert.Single(headerValues); + + var userAgent = headerValues[0]; + + Assert.Equal("User Value", userAgent); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + } + } + } + [ConditionalFact] [WebSocketsSupportedCondition] public async Task WebSocketOptionsAreApplied() diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Transport.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Transport.cs index 142e40546c..0244af0afd 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Transport.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Transport.cs @@ -3,6 +3,7 @@ using System; using System.IO.Pipelines; +using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; @@ -113,16 +114,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests testHttpHandler.OnRequest(async (request, next, token) => { - var userAgentHeaderCollection = request.Headers.UserAgent; - var userAgentHeader = Assert.Single(userAgentHeaderCollection); - Assert.Equal("Microsoft.AspNetCore.Http.Connections.Client", userAgentHeader.Product.Name); + var userAgentHeader = request.Headers.UserAgent.ToString(); + + Assert.NotNull(userAgentHeader); + Assert.StartsWith("Microsoft SignalR/", userAgentHeader); // user agent version should come from version embedded in assembly metadata var assemblyVersion = typeof(Constants) .Assembly .GetCustomAttribute<AssemblyInformationalVersionAttribute>(); - Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version); + Assert.Contains(assemblyVersion.InformationalVersion, userAgentHeader); requestsExecuted = true; diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs index 2c9df93cb8..b303e71ddd 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs @@ -414,6 +414,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + [Fact] + public async Task CanAwaitUsingHubConnection() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + await using var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); + } + } + private class SampleObject { public SampleObject(string foo, int bar) diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp.cs b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp.cs new file mode 100644 index 0000000000..35b6c7cc35 --- /dev/null +++ b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Connections.Client +{ + public partial class HttpConnection : Microsoft.AspNetCore.Connections.ConnectionContext, Microsoft.AspNetCore.Connections.Features.IConnectionInherentKeepAliveFeature + { + public HttpConnection(Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions httpConnectionOptions, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + public HttpConnection(System.Uri url) { } + public HttpConnection(System.Uri url, Microsoft.AspNetCore.Http.Connections.HttpTransportType transports) { } + public HttpConnection(System.Uri url, Microsoft.AspNetCore.Http.Connections.HttpTransportType transports, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + public override string ConnectionId { get { throw null; } set { } } + public override Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public override System.Collections.Generic.IDictionary<object, object> Items { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + bool Microsoft.AspNetCore.Connections.Features.IConnectionInherentKeepAliveFeature.HasInherentKeepAlive { get { throw null; } } + public override System.IO.Pipelines.IDuplexPipe Transport { get { throw null; } set { } } + [System.Diagnostics.DebuggerStepThroughAttribute] + public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public partial class HttpConnectionOptions + { + public HttpConnectionOptions() { } + public System.Func<System.Threading.Tasks.Task<string>> AccessTokenProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates { get { throw null; } set { } } + public System.TimeSpan CloseTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Net.CookieContainer Cookies { get { throw null; } set { } } + public System.Net.ICredentials Credentials { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Collections.Generic.IDictionary<string, string> Headers { get { throw null; } set { } } + public System.Func<System.Net.Http.HttpMessageHandler, System.Net.Http.HttpMessageHandler> HttpMessageHandlerFactory { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Net.IWebProxy Proxy { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool SkipNegotiation { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public Microsoft.AspNetCore.Http.Connections.HttpTransportType Transports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Uri Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool? UseDefaultCredentials { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Action<System.Net.WebSockets.ClientWebSocketOptions> WebSocketConfiguration { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + public partial class NoTransportSupportedException : System.Exception + { + public NoTransportSupportedException(string message) { } + } + public partial class TransportFailedException : System.Exception + { + public TransportFailedException(string transportType, string message, System.Exception innerException = null) { } + public string TransportType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } +} diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs index 1fb9ba10aa..e014061f50 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs @@ -561,14 +561,34 @@ namespace Microsoft.AspNetCore.Http.Connections.Client httpClient.Timeout = HttpClientTimeout; // Start with the user agent header - httpClient.DefaultRequestHeaders.UserAgent.Add(Constants.UserAgentHeader); + httpClient.DefaultRequestHeaders.Add(Constants.UserAgent, Constants.UserAgentHeader); // Apply any headers configured on the HttpConnectionOptions if (_httpConnectionOptions?.Headers != null) { foreach (var header in _httpConnectionOptions.Headers) { - httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + // Check if the key is User-Agent and remove if empty string then replace if it exists. + if (string.Equals(header.Key, Constants.UserAgent, StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(header.Value)) + { + httpClient.DefaultRequestHeaders.Remove(header.Key); + } + else if (httpClient.DefaultRequestHeaders.Contains(header.Key)) + { + httpClient.DefaultRequestHeaders.Remove(header.Key); + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + else + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + else + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } } } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Constants.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Constants.cs index 22b41d56f3..c99c7db1a0 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Constants.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Constants.cs @@ -3,19 +3,18 @@ using System.Diagnostics; using System.Linq; -using System.Net.Http.Headers; using System.Reflection; +using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Http.Connections.Client.Internal { internal static class Constants { - public static readonly ProductInfoHeaderValue UserAgentHeader; + public const string UserAgent = "User-Agent"; + public static readonly string UserAgentHeader; static Constants() { - var userAgent = "Microsoft.AspNetCore.Http.Connections.Client"; - var assemblyVersion = typeof(Constants) .Assembly .GetCustomAttributes<AssemblyInformationalVersionAttribute>() @@ -23,14 +22,22 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal Debug.Assert(assemblyVersion != null); + var majorVersion = typeof(Constants).Assembly.GetName().Version.Major; + var minorVersion = typeof(Constants).Assembly.GetName().Version.Minor; + var os = RuntimeInformation.OSDescription; + var runtime = ".NET"; + var runtimeVersion = RuntimeInformation.FrameworkDescription; + // assembly version attribute should always be present // but in case it isn't then don't include version in user-agent if (assemblyVersion != null) { - userAgent += "/" + assemblyVersion.InformationalVersion; + UserAgentHeader = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; {os}; {runtime}; {runtimeVersion})"; + } + else + { + UserAgentHeader = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({os}; {runtime}; {runtimeVersion})"; } - - UserAgentHeader = ProductInfoHeaderValue.Parse(userAgent); } } } diff --git a/src/SignalR/clients/java/signalr/build.gradle b/src/SignalR/clients/java/signalr/build.gradle index 61b170a40d..8feab7b9b7 100644 --- a/src/SignalR/clients/java/signalr/build.gradle +++ b/src/SignalR/clients/java/signalr/build.gradle @@ -108,3 +108,27 @@ task generatePOM { } task createPackage(dependsOn: [jar,sourceJar,javadocJar,generatePOM]) + +task generateVersionClass { + inputs.property "version", project.version + outputs.dir "$buildDir/generated" + doFirst { + def versionFile = file("$buildDir/../src/main/java/com/microsoft/signalr/Version.java") + versionFile.parentFile.mkdirs() + versionFile.text = + """ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +package com.microsoft.signalr; + +class Version { + public static String getDetailedVersion() { + return "$project.version"; + } +} +""" + } +} + +compileJava.dependsOn generateVersionClass diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java index 36dca4f808..03390ab986 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java @@ -328,6 +328,7 @@ public class HubConnection { handshakeResponseSubject = CompletableSubject.create(); handshakeReceived = false; CompletableSubject tokenCompletable = CompletableSubject.create(); + localHeaders.put(UserAgentHelper.getUserAgentName(), UserAgentHelper.createUserAgentString()); if (headers != null) { this.localHeaders.putAll(headers); } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/UserAgentHelper.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/UserAgentHelper.java new file mode 100644 index 0000000000..e54809c700 --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/UserAgentHelper.java @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +package com.microsoft.signalr; + +public class UserAgentHelper { + + private final static String USER_AGENT = "User-Agent"; + + public static String getUserAgentName() { + return USER_AGENT; + } + + public static String createUserAgentString() { + StringBuilder agentBuilder = new StringBuilder("Microsoft SignalR/"); + + // Parsing version numbers + String detailedVersion = Version.getDetailedVersion(); + agentBuilder.append(getVersion(detailedVersion)); + agentBuilder.append(" ("); + agentBuilder.append(detailedVersion); + agentBuilder.append("; "); + + // Getting the OS name + agentBuilder.append(getOS()); + agentBuilder.append("; Java; "); + + // Vendor and Version + agentBuilder.append(getJavaVersion()); + agentBuilder.append("; "); + agentBuilder.append(getJavaVendor()); + agentBuilder.append(")"); + + return agentBuilder.toString(); + } + + static String getVersion(String detailedVersion) { + // Getting the index of the second . so we can return just the major and minor version. + int shortVersionIndex = detailedVersion.indexOf(".", detailedVersion.indexOf(".") + 1); + return detailedVersion.substring(0, shortVersionIndex); + } + + static String getJavaVendor() { + return System.getProperty("java.vendor"); + } + + static String getJavaVersion() { + return System.getProperty("java.version"); + } + + static String getOS() { + return System.getProperty("os.name"); + } +} diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Version.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Version.java new file mode 100644 index 0000000000..abafa2e53b --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Version.java @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +package com.microsoft.signalr; + +class Version { + public static String getDetailedVersion() { + return "99.99.99-dev"; + } +} diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java index 3f1d7d5b3f..da28f279a4 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java @@ -1116,7 +1116,7 @@ class HubConnectionTest { hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); AtomicBoolean done = new AtomicBoolean(); - Single<String> result = hubConnection.invoke(String.class, "fixedMessage", null); + Single<String> result = hubConnection.invoke(String.class, "fixedMessage", (Object)null); result.doOnSuccess(value -> done.set(true)).subscribe(); assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); assertFalse(done.get()); @@ -2267,6 +2267,77 @@ class HubConnectionTest { } @Test + public void userAgentHeaderIsSet() { + AtomicReference<String> header = new AtomicReference<>(); + TestHttpClient client = new TestHttpClient() + .on("POST", "http://example.com/negotiate?negotiateVersion=1", + (req) -> { + header.set(req.getHeaders().get("User-Agent")); + return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + }); + + MockTransport transport = new MockTransport(); + HubConnection hubConnection = HubConnectionBuilder.create("http://example.com") + .withTransportImplementation(transport) + .withHttpClient(client) + .build(); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); + hubConnection.stop(); + + assertTrue(header.get().startsWith("Microsoft SignalR/")); + } + + @Test + public void userAgentHeaderCanBeOverwritten() { + AtomicReference<String> header = new AtomicReference<>(); + TestHttpClient client = new TestHttpClient() + .on("POST", "http://example.com/negotiate?negotiateVersion=1", + (req) -> { + header.set(req.getHeaders().get("User-Agent")); + return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + }); + + MockTransport transport = new MockTransport(); + HubConnection hubConnection = HubConnectionBuilder.create("http://example.com") + .withTransportImplementation(transport) + .withHttpClient(client) + .withHeader("User-Agent", "Updated Value") + .build(); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); + hubConnection.stop(); + assertEquals("Updated Value", header.get()); + } + + @Test + public void userAgentCanBeCleared() { + AtomicReference<String> header = new AtomicReference<>(); + TestHttpClient client = new TestHttpClient() + .on("POST", "http://example.com/negotiate?negotiateVersion=1", + (req) -> { + header.set(req.getHeaders().get("User-Agent")); + return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" + + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + }); + + MockTransport transport = new MockTransport(); + HubConnection hubConnection = HubConnectionBuilder.create("http://example.com") + .withTransportImplementation(transport) + .withHttpClient(client) + .withHeader("User-Agent", "") + .build(); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); + hubConnection.stop(); + assertEquals("", header.get()); + } + @Test public void headersAreSetAndSentThroughBuilder() { AtomicReference<String> header = new AtomicReference<>(); TestHttpClient client = new TestHttpClient() diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/UserAgentTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/UserAgentTest.java new file mode 100644 index 0000000000..1f92edc07b --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/UserAgentTest.java @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +package com.microsoft.signalr; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class UserAgentTest { + + private static Stream<Arguments> Versions() { + return Stream.of( + Arguments.of("1.0.0", "1.0"), + Arguments.of("3.1.4-preview9-12345", "3.1"), + Arguments.of("3.1.4-preview9-12345-extrastuff", "3.1"), + Arguments.of("99.99.99-dev", "99.99")); + } + + @ParameterizedTest + @MethodSource("Versions") + public void getVersionFromDetailedVersion(String detailedVersion, String version) { + assertEquals(version, UserAgentHelper.getVersion(detailedVersion)); + } + + @Test + public void verifyJavaVendor() { + assertEquals(System.getProperty("java.vendor"), UserAgentHelper.getJavaVendor()); + } + + @Test + public void verifyJavaVersion() { + assertEquals(System.getProperty("java.version"), UserAgentHelper.getJavaVersion()); + } + + @Test + public void checkUserAgentString() { + String userAgent = UserAgentHelper.createUserAgentString(); + assertNotNull(userAgent); + + String detailedVersion = Version.getDetailedVersion(); + String handMadeUserAgent = "Microsoft SignalR/" + UserAgentHelper.getVersion(detailedVersion) + + " (" + detailedVersion + "; " + UserAgentHelper.getOS() + "; Java; " + + UserAgentHelper.getJavaVersion() + "; " + UserAgentHelper.getJavaVendor() + ")"; + + assertEquals(handMadeUserAgent, userAgent); + } +} diff --git a/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts b/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts index 5720a4e30e..ad6f65db08 100644 --- a/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts +++ b/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts @@ -245,7 +245,7 @@ function runJest(httpsUrl: string, httpUrl: string) { (async () => { try { - const serverPath = path.resolve(ARTIFACTS_DIR, "bin", "SignalR.Client.FunctionalTestApp", configuration, "netcoreapp3.1", "SignalR.Client.FunctionalTestApp.dll"); + const serverPath = path.resolve(ARTIFACTS_DIR, "bin", "SignalR.Client.FunctionalTestApp", configuration, "netcoreapp5.0", "SignalR.Client.FunctionalTestApp.dll"); debug(`Launching Functional Test Server: ${serverPath}`); let desiredServerUrl = "https://127.0.0.1:0;http://127.0.0.1:0"; diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts b/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts index 2bb33c1c11..c66bff8e49 100644 --- a/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts +++ b/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts @@ -1,8 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -import { HttpTransportType, IHubProtocol, JsonHubProtocol } from "@microsoft/signalr"; +import { HttpClient, HttpTransportType, IHubProtocol, JsonHubProtocol } from "@microsoft/signalr"; import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; +import { TestLogger } from "./TestLogger"; + +import { FetchHttpClient } from "@microsoft/signalr/dist/esm/FetchHttpClient"; +import { NodeHttpClient } from "@microsoft/signalr/dist/esm/NodeHttpClient"; +import { Platform } from "@microsoft/signalr/dist/esm/Utils"; +import { XhrHttpClient } from "@microsoft/signalr/dist/esm/XhrHttpClient"; // On slower CI machines, these tests sometimes take longer than 5s jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000; @@ -97,6 +103,34 @@ export function eachTransportAndProtocol(action: (transport: HttpTransportType, }); } +export function eachTransportAndProtocolAndHttpClient(action: (transport: HttpTransportType, protocol: IHubProtocol, httpClient: HttpClient) => void) { + eachTransportAndProtocol((transport, protocol) => { + getHttpClients().forEach((httpClient) => { + action(transport, protocol, httpClient); + }); + }); +} + export function getGlobalObject(): any { return typeof window !== "undefined" ? window : global; } + +export function getHttpClients(): HttpClient[] { + const httpClients: HttpClient[] = []; + if (typeof XMLHttpRequest !== "undefined") { + httpClients.push(new XhrHttpClient(TestLogger.instance)); + } + if (typeof fetch !== "undefined") { + httpClients.push(new FetchHttpClient(TestLogger.instance)); + } + if (Platform.isNode) { + httpClients.push(new NodeHttpClient(TestLogger.instance)); + } + return httpClients; +} + +export function eachHttpClient(action: (transport: HttpClient) => void) { + return getHttpClients().forEach((t) => { + return action(t); + }); +} diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts b/src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts index 7760294123..3b559af265 100644 --- a/src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts +++ b/src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts @@ -5,7 +5,7 @@ // tslint:disable:no-floating-promises import { HttpTransportType, IHttpConnectionOptions, TransferFormat } from "@microsoft/signalr"; -import { eachTransport, ECHOENDPOINT_URL } from "./Common"; +import { eachHttpClient, eachTransport, ECHOENDPOINT_URL } from "./Common"; import { TestLogger } from "./TestLogger"; // We want to continue testing HttpConnection, but we don't export it anymore. So just pull it in directly from the source file. @@ -44,109 +44,114 @@ describe("connection", () => { }); eachTransport((transportType) => { - describe(`over ${HttpTransportType[transportType]}`, () => { - it("can send and receive messages", (done) => { - const message = "Hello World!"; - // the url should be resolved relative to the document.location.host - // and the leading '/' should be automatically added to the url - const connection = new HttpConnection(ECHOENDPOINT_URL, { - ...commonOptions, - transport: transportType, + eachHttpClient((httpClient) => { + describe(`over ${HttpTransportType[transportType]} with ${(httpClient.constructor as any).name}`, () => { + it("can send and receive messages", (done) => { + const message = "Hello World!"; + // the url should be resolved relative to the document.location.host + // and the leading '/' should be automatically added to the url + const connection = new HttpConnection(ECHOENDPOINT_URL, { + ...commonOptions, + httpClient, + transport: transportType, + }); + + connection.onreceive = (data: any) => { + if (data === message) { + connection.stop(); + } + }; + + connection.onclose = (error: any) => { + expect(error).toBeUndefined(); + done(); + }; + + connection.start(TransferFormat.Text).then(() => { + connection.send(message); + }).catch((e: any) => { + fail(e); + done(); + }); }); - connection.onreceive = (data: any) => { - if (data === message) { - connection.stop(); - } - }; - - connection.onclose = (error: any) => { - expect(error).toBeUndefined(); - done(); - }; - - connection.start(TransferFormat.Text).then(() => { - connection.send(message); - }).catch((e: any) => { - fail(e); - done(); - }); - }); + it("does not log content of messages sent or received by default", (done) => { + TestLogger.saveLogsAndReset(); + const message = "Hello World!"; - it("does not log content of messages sent or received by default", (done) => { - TestLogger.saveLogsAndReset(); - const message = "Hello World!"; + // DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is not set. + const connection = new HttpConnection(ECHOENDPOINT_URL, { + httpClient, + logger: TestLogger.instance, + transport: transportType, + }); - // DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is not set. - const connection = new HttpConnection(ECHOENDPOINT_URL, { - logger: TestLogger.instance, - transport: transportType, - }); - - connection.onreceive = (data: any) => { - if (data === message) { - connection.stop(); - } - }; - - // @ts-ignore: We don't use the error parameter intentionally. - connection.onclose = (error) => { - // Search the logs for the message content - expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0); - // @ts-ignore: We don't use the _ or __ parameters intentionally. - for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) { - expect(logMessage).not.toContain(message); - } - done(); - }; - - connection.start(TransferFormat.Text).then(() => { - connection.send(message); - }).catch((e) => { - fail(e); - done(); - }); - }); - - it("does log content of messages sent or received when enabled", (done) => { - TestLogger.saveLogsAndReset(); - const message = "Hello World!"; - - // DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is set to true (even if commonOptions changes). - const connection = new HttpConnection(ECHOENDPOINT_URL, { - logMessageContent: true, - logger: TestLogger.instance, - transport: transportType, + connection.onreceive = (data: any) => { + if (data === message) { + connection.stop(); + } + }; + + // @ts-ignore: We don't use the error parameter intentionally. + connection.onclose = (error) => { + // Search the logs for the message content + expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0); + // @ts-ignore: We don't use the _ or __ parameters intentionally. + for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) { + expect(logMessage).not.toContain(message); + } + done(); + }; + + connection.start(TransferFormat.Text).then(() => { + connection.send(message); + }).catch((e) => { + fail(e); + done(); + }); }); - connection.onreceive = (data: any) => { - if (data === message) { - connection.stop(); - } - }; - - // @ts-ignore: We don't use the error parameter intentionally. - connection.onclose = (error) => { - // Search the logs for the message content - let matches = 0; - expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0); - // @ts-ignore: We don't use the _ or __ parameters intentionally. - for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) { - if (logMessage.indexOf(message) !== -1) { - matches += 1; + it("does log content of messages sent or received when enabled", (done) => { + TestLogger.saveLogsAndReset(); + const message = "Hello World!"; + + // DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is set to true (even if commonOptions changes). + const connection = new HttpConnection(ECHOENDPOINT_URL, { + httpClient, + logMessageContent: true, + logger: TestLogger.instance, + transport: transportType, + }); + + connection.onreceive = (data: any) => { + if (data === message) { + connection.stop(); + } + }; + + // @ts-ignore: We don't use the error parameter intentionally. + connection.onclose = (error) => { + // Search the logs for the message content + let matches = 0; + expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0); + // @ts-ignore: We don't use the _ or __ parameters intentionally. + for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) { + if (logMessage.indexOf(message) !== -1) { + matches += 1; + } } - } - - // One match for send, one for receive. - expect(matches).toEqual(2); - done(); - }; - - connection.start(TransferFormat.Text).then(() => { - connection.send(message); - }).catch((e: any) => { - fail(e); - done(); + + // One match for send, one for receive. + expect(matches).toEqual(2); + done(); + }; + + connection.start(TransferFormat.Text).then(() => { + connection.send(message); + }).catch((e: any) => { + fail(e); + done(); + }); }); }); }); diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts b/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts index 3d5f434a17..dbfa1a886f 100644 --- a/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts +++ b/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts @@ -7,7 +7,7 @@ import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr"; import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; -import { eachTransport, eachTransportAndProtocol, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common"; +import { eachTransport, eachTransportAndProtocolAndHttpClient, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common"; import "./LogBannerReporter"; import { TestLogger } from "./TestLogger"; @@ -49,12 +49,12 @@ function getConnectionBuilder(transportType?: HttpTransportType, url?: string, o } describe("hubConnection", () => { - eachTransportAndProtocol((transportType, protocol) => { + eachTransportAndProtocolAndHttpClient((transportType, protocol, httpClient) => { describe("using " + protocol.name + " over " + HttpTransportType[transportType] + " transport", () => { it("can invoke server method and receive result", (done) => { const message = "你好,世界!"; - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -81,7 +81,7 @@ describe("hubConnection", () => { it("using https, can invoke server method and receive result", (done) => { const message = "你好,世界!"; - const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL) + const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL, { httpClient }) .withHubProtocol(protocol) .build(); @@ -108,7 +108,7 @@ describe("hubConnection", () => { it("can invoke server method non-blocking and not receive result", (done) => { const message = "你好,世界!"; - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -130,7 +130,7 @@ describe("hubConnection", () => { }); it("can invoke server method structural object and receive structural result", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -154,7 +154,7 @@ describe("hubConnection", () => { }); it("can stream server method and receive result", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -185,7 +185,7 @@ describe("hubConnection", () => { }); it("can stream server method and cancel stream", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -219,7 +219,7 @@ describe("hubConnection", () => { it("rethrows an exception from the server when invoking", (done) => { const errorMessage = "An unexpected error occurred invoking 'ThrowException' on the server. InvalidOperationException: An error occurred."; - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -241,7 +241,7 @@ describe("hubConnection", () => { }); it("throws an exception when invoking streaming method with invoke", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -263,7 +263,7 @@ describe("hubConnection", () => { }); it("throws an exception when receiving a streaming result for method called with invoke", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -286,7 +286,7 @@ describe("hubConnection", () => { it("rethrows an exception from the server when streaming", (done) => { const errorMessage = "An unexpected error occurred invoking 'StreamThrowException' on the server. InvalidOperationException: An error occurred."; - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -313,7 +313,7 @@ describe("hubConnection", () => { }); it("throws an exception when invoking hub method with stream", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -340,7 +340,7 @@ describe("hubConnection", () => { }); it("can receive server calls", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -370,7 +370,7 @@ describe("hubConnection", () => { }); it("can receive server calls without rebinding handler when restarted", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -425,7 +425,7 @@ describe("hubConnection", () => { }); it("closed with error or start fails if hub cannot be created", async (done) => { - const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/uncreatable") + const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/uncreatable", { httpClient }) .withHubProtocol(protocol) .build(); @@ -446,7 +446,7 @@ describe("hubConnection", () => { }); it("can handle different types", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -489,7 +489,7 @@ describe("hubConnection", () => { }); it("can receive different types", (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -534,7 +534,7 @@ describe("hubConnection", () => { it("can be restarted", (done) => { const message = "你好,世界!"; - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -577,7 +577,7 @@ describe("hubConnection", () => { }); it("can stream from client to server with rxjs", async (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); @@ -594,7 +594,7 @@ describe("hubConnection", () => { }); it("can stream from client to server and close with error with rxjs", async (done) => { - const hubConnection = getConnectionBuilder(transportType) + const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient }) .withHubProtocol(protocol) .build(); diff --git a/src/SignalR/clients/ts/signalr-protocol-msgpack/README.md b/src/SignalR/clients/ts/signalr-protocol-msgpack/README.md index e840374319..00856c5496 100644 --- a/src/SignalR/clients/ts/signalr-protocol-msgpack/README.md +++ b/src/SignalR/clients/ts/signalr-protocol-msgpack/README.md @@ -12,7 +12,7 @@ yarn add @microsoft/signalr-protocol-msgpack ## Usage -See the [SignalR Documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr) at docs.microsoft.com for documentation on the latest release. [API Reference Documentation](https://docs.microsoft.com/javascript/api/%40aspnet/signalr-protocol-msgpack/?view=signalr-js-latest) is also available on docs.microsoft.com. +See the [SignalR Documentation](https://docs.microsoft.com/aspnet/core/signalr) at docs.microsoft.com for documentation on the latest release. [API Reference Documentation](https://docs.microsoft.com/javascript/api/%40aspnet/signalr-protocol-msgpack/?view=signalr-js-latest) is also available on docs.microsoft.com. ### Browser diff --git a/src/SignalR/clients/ts/signalr-protocol-msgpack/package.json b/src/SignalR/clients/ts/signalr-protocol-msgpack/package.json index 3d262cc056..d53083aa27 100644 --- a/src/SignalR/clients/ts/signalr-protocol-msgpack/package.json +++ b/src/SignalR/clients/ts/signalr-protocol-msgpack/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/signalr-protocol-msgpack", - "version": "3.0.0-dev", + "version": "5.0.0-dev", "description": "MsgPack Protocol support for ASP.NET Core SignalR", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/SignalR/clients/ts/signalr-protocol-msgpack/signalr-protocol-msgpack.npmproj b/src/SignalR/clients/ts/signalr-protocol-msgpack/signalr-protocol-msgpack.npmproj index 72978faa2d..1a2b2deac3 100644 --- a/src/SignalR/clients/ts/signalr-protocol-msgpack/signalr-protocol-msgpack.npmproj +++ b/src/SignalR/clients/ts/signalr-protocol-msgpack/signalr-protocol-msgpack.npmproj @@ -13,5 +13,9 @@ <ProjectReference Include="..\signalr\signalr.npmproj" /> </ItemGroup> + <ItemGroup> + <BuildOutputFiles Include="dist\browser\signalr-protocol-msgpack.js" /> + </ItemGroup> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.targets))\Directory.Build.targets" /> </Project> diff --git a/src/SignalR/clients/ts/signalr/README.md b/src/SignalR/clients/ts/signalr/README.md index ec8f34b227..3bf0d6534f 100644 --- a/src/SignalR/clients/ts/signalr/README.md +++ b/src/SignalR/clients/ts/signalr/README.md @@ -1,4 +1,4 @@ -JavaScript and TypeScript clients for SignalR for ASP.NET Core +JavaScript and TypeScript clients for SignalR for ASP.NET Core and Azure SignalR Service ## Installation @@ -12,7 +12,9 @@ yarn add @microsoft/signalr ## Usage -See the [SignalR Documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr) at docs.microsoft.com for documentation on the latest release. [API Reference Documentation](https://docs.microsoft.com/javascript/api/%40aspnet/signalr/?view=signalr-js-latest) is also available on docs.microsoft.com. +See the [SignalR Documentation](https://docs.microsoft.com/aspnet/core/signalr) at docs.microsoft.com for documentation on the latest release. [API Reference Documentation](https://docs.microsoft.com/javascript/api/%40aspnet/signalr/?view=signalr-js-latest) is also available on docs.microsoft.com. + +For documentation on using this client with Azure SignalR Service and Azure Functions, see the [SignalR Service serverless developer guide](https://docs.microsoft.com/azure/azure-signalr/signalr-concept-serverless-development-config). ### Browser diff --git a/src/SignalR/clients/ts/signalr/package.json b/src/SignalR/clients/ts/signalr/package.json index 6b50f24d35..7fcfc9fb29 100644 --- a/src/SignalR/clients/ts/signalr/package.json +++ b/src/SignalR/clients/ts/signalr/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/signalr", - "version": "3.0.0-dev", + "version": "5.0.0-dev", "description": "ASP.NET Core SignalR Client", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/SignalR/clients/ts/signalr/signalr.npmproj b/src/SignalR/clients/ts/signalr/signalr.npmproj index e6a6c1d993..dbd62e31c6 100644 --- a/src/SignalR/clients/ts/signalr/signalr.npmproj +++ b/src/SignalR/clients/ts/signalr/signalr.npmproj @@ -8,5 +8,10 @@ <IsShippingPackage>true</IsShippingPackage> </PropertyGroup> + <ItemGroup> + <BuildOutputFiles Include="dist\browser\signalr.js" /> + <BuildOutputFiles Include="dist\webworker\signalr.js" /> + </ItemGroup> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.targets))\Directory.Build.targets" /> </Project> diff --git a/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts b/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts index fece43020d..8058e5716a 100644 --- a/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts +++ b/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. import { AbortError } from "./Errors"; +import { FetchHttpClient } from "./FetchHttpClient"; import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient"; import { ILogger } from "./ILogger"; import { NodeHttpClient } from "./NodeHttpClient"; @@ -15,7 +16,9 @@ export class DefaultHttpClient extends HttpClient { public constructor(logger: ILogger) { super(); - if (typeof XMLHttpRequest !== "undefined") { + if (typeof fetch !== "undefined") { + this.httpClient = new FetchHttpClient(logger); + } else if (typeof XMLHttpRequest !== "undefined") { this.httpClient = new XhrHttpClient(logger); } else { this.httpClient = new NodeHttpClient(logger); diff --git a/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts b/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts new file mode 100644 index 0000000000..48840b42c2 --- /dev/null +++ b/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +import { AbortError, HttpError, TimeoutError } from "./Errors"; +import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient"; +import { ILogger, LogLevel } from "./ILogger"; + +export class FetchHttpClient extends HttpClient { + private readonly logger: ILogger; + + public constructor(logger: ILogger) { + super(); + this.logger = logger; + } + + /** @inheritDoc */ + public async send(request: HttpRequest): Promise<HttpResponse> { + // Check that abort was not signaled before calling send + if (request.abortSignal && request.abortSignal.aborted) { + throw new AbortError(); + } + + if (!request.method) { + throw new Error("No method defined."); + } + if (!request.url) { + throw new Error("No url defined."); + } + + const abortController = new AbortController(); + + let error: any; + // Hook our abortSignal into the abort controller + if (request.abortSignal) { + request.abortSignal.onabort = () => { + abortController.abort(); + error = new AbortError(); + }; + } + + // If a timeout has been passed in, setup a timeout to call abort + // Type needs to be any to fit window.setTimeout and NodeJS.setTimeout + let timeoutId: any = null; + if (request.timeout) { + const msTimeout = request.timeout!; + timeoutId = setTimeout(() => { + abortController.abort(); + this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`); + error = new TimeoutError(); + }, msTimeout); + } + + let response: Response; + try { + response = await fetch(request.url!, { + body: request.content!, + cache: "no-cache", + credentials: "include", + headers: { + "Content-Type": "text/plain;charset=UTF-8", + "X-Requested-With": "XMLHttpRequest", + ...request.headers, + }, + method: request.method!, + mode: "cors", + redirect: "manual", + signal: abortController.signal, + }); + } catch (e) { + if (error) { + throw error; + } + this.logger.log( + LogLevel.Warning, + `Error from HTTP request. ${e}.`, + ); + throw e; + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (request.abortSignal) { + request.abortSignal.onabort = null; + } + } + + if (!response.ok) { + throw new HttpError(response.statusText, response.status); + } + + const content = deserializeContent(response, request.responseType); + const payload = await content; + + return new HttpResponse( + response.status, + response.statusText, + payload, + ); + } +} + +function deserializeContent(response: Response, responseType?: XMLHttpRequestResponseType): Promise<string | ArrayBuffer> { + let content; + switch (responseType) { + case "arraybuffer": + content = response.arrayBuffer(); + break; + case "text": + content = response.text(); + break; + case "blob": + case "document": + case "json": + throw new Error(`${responseType} is not supported.`); + default: + content = response.text(); + break; + } + + return content; +} diff --git a/src/SignalR/clients/ts/signalr/src/HttpClient.ts b/src/SignalR/clients/ts/signalr/src/HttpClient.ts index 9685feca5e..c50289bbf1 100644 --- a/src/SignalR/clients/ts/signalr/src/HttpClient.ts +++ b/src/SignalR/clients/ts/signalr/src/HttpClient.ts @@ -57,6 +57,14 @@ export class HttpResponse { * @param {ArrayBuffer} content The content of the response. */ constructor(statusCode: number, statusText: string, content: ArrayBuffer); + + /** Constructs a new instance of {@link @microsoft/signalr.HttpResponse} with the specified status code, message and binary content. + * + * @param {number} statusCode The status code of the response. + * @param {string} statusText The status message of the response. + * @param {string | ArrayBuffer} content The content of the response. + */ + constructor(statusCode: number, statusText: string, content: string | ArrayBuffer); constructor( public readonly statusCode: number, public readonly statusText?: string, diff --git a/src/SignalR/clients/ts/signalr/src/HubConnectionBuilder.ts b/src/SignalR/clients/ts/signalr/src/HubConnectionBuilder.ts index 98a23a3b3d..fa5d7432b9 100644 --- a/src/SignalR/clients/ts/signalr/src/HubConnectionBuilder.ts +++ b/src/SignalR/clients/ts/signalr/src/HubConnectionBuilder.ts @@ -70,14 +70,14 @@ export class HubConnectionBuilder { /** Configures custom logging for the {@link @microsoft/signalr.HubConnection}. * * @param {string} logLevel A string representing a LogLevel setting a minimum level of messages to log. - * See {@link https://docs.microsoft.com/en-us/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details. + * See {@link https://docs.microsoft.com/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details. */ public configureLogging(logLevel: string): HubConnectionBuilder; /** Configures custom logging for the {@link @microsoft/signalr.HubConnection}. * * @param {LogLevel | string | ILogger} logging A {@link @microsoft/signalr.LogLevel}, a string representing a LogLevel, or an object implementing the {@link @microsoft/signalr.ILogger} interface. - * See {@link https://docs.microsoft.com/en-us/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details. + * See {@link https://docs.microsoft.com/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details. * @returns The {@link @microsoft/signalr.HubConnectionBuilder} instance, for chaining. */ public configureLogging(logging: LogLevel | string | ILogger): HubConnectionBuilder; diff --git a/src/SignalR/common/Http.Connections.Common/src/Microsoft.AspNetCore.Http.Connections.Common.csproj b/src/SignalR/common/Http.Connections.Common/src/Microsoft.AspNetCore.Http.Connections.Common.csproj index 521467f849..c40fa68e6d 100644 --- a/src/SignalR/common/Http.Connections.Common/src/Microsoft.AspNetCore.Http.Connections.Common.csproj +++ b/src/SignalR/common/Http.Connections.Common/src/Microsoft.AspNetCore.Http.Connections.Common.csproj @@ -24,7 +24,7 @@ <Reference Include="System.Text.Json" /> </ItemGroup> - <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.1'"> + <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'"> <!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <SuppressBaselineReference Include="Newtonsoft.Json" /> diff --git a/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs b/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs index a98e0ba94c..ae69b56cdd 100644 --- a/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs +++ b/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs @@ -264,13 +264,11 @@ namespace Microsoft.AspNetCore.Http.Connections switch (reader.TokenType) { case JsonTokenType.PropertyName: - var memberName = reader.ValueSpan; - - if (memberName.SequenceEqual(TransportPropertyNameBytes.EncodedUtf8Bytes)) + if (reader.ValueTextEquals(TransportPropertyNameBytes.EncodedUtf8Bytes)) { availableTransport.Transport = reader.ReadAsString(TransportPropertyName); } - else if (memberName.SequenceEqual(TransferFormatsPropertyNameBytes.EncodedUtf8Bytes)) + else if (reader.ValueTextEquals(TransferFormatsPropertyNameBytes.EncodedUtf8Bytes)) { reader.CheckRead(); reader.EnsureArrayStart(); diff --git a/src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs b/src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs index 704f0f4d27..00d803ffdd 100644 --- a/src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs +++ b/src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs @@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests [InlineData("{\"url\": \"http://foo.com/chat\"}", null, null, "http://foo.com/chat", null, 0, null)] [InlineData("{\"url\": \"http://foo.com/chat\", \"accessToken\": \"token\"}", null, null, "http://foo.com/chat", "token", 0, null)] [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0, null)] + [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0, null)] + [InlineData("{\"negotiateVersion\":123,\"connectionId\":\"123\",\"connectionToken\":\"789\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 123, "789")] [InlineData("{\"negotiateVersion\":123,\"negotiateVersion\":321, \"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 321, "789")] [InlineData("{\"ignore\":123,\"negotiateVersion\":123, \"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 123, "789")] [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[],\"negotiateVersion\":123, \"connectionToken\":\"789\"}", "123", new string[0], null, null, 123, "789")] diff --git a/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj b/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj index 27ac41a9df..2dd22c44a5 100644 --- a/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj +++ b/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj @@ -23,7 +23,7 @@ <Reference Include="Microsoft.AspNetCore.SignalR.Common" /> </ItemGroup> - <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.1'"> + <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'"> <!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <SuppressBaselineReference Include="Newtonsoft.Json" /> </ItemGroup> diff --git a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs index 884e427b68..28596c6210 100644 --- a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs +++ b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.ExceptionServices; +using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Internal; @@ -551,19 +552,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol writer.WriteStartArray(ArgumentsPropertyNameBytes); foreach (var argument in arguments) { - var type = argument?.GetType(); - if (type == typeof(DateTime)) - { - writer.WriteStringValue((DateTime)argument); - } - else if (type == typeof(DateTimeOffset)) - { - writer.WriteStringValue((DateTimeOffset)argument); - } - else - { - JsonSerializer.Serialize(writer, argument, type, _payloadSerializerOptions); - } + JsonSerializer.Serialize(writer, argument, argument?.GetType(), _payloadSerializerOptions); } writer.WriteEndArray(); } @@ -746,19 +735,20 @@ namespace Microsoft.AspNetCore.SignalR.Protocol internal static JsonSerializerOptions CreateDefaultSerializerSettings() { - var options = new JsonSerializerOptions(); - options.WriteIndented = false; - options.ReadCommentHandling = JsonCommentHandling.Disallow; - options.AllowTrailingCommas = false; - options.IgnoreNullValues = false; - options.IgnoreReadOnlyProperties = false; - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - options.PropertyNameCaseInsensitive = true; - options.MaxDepth = 64; - options.DictionaryKeyPolicy = null; - options.DefaultBufferSize = 16 * 1024; - - return options; + return new JsonSerializerOptions() + { + WriteIndented = false, + ReadCommentHandling = JsonCommentHandling.Disallow, + AllowTrailingCommas = false, + IgnoreNullValues = false, + IgnoreReadOnlyProperties = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + MaxDepth = 64, + DictionaryKeyPolicy = null, + DefaultBufferSize = 16 * 1024, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; } } } diff --git a/src/SignalR/common/Shared/ReusableUtf8JsonWriter.cs b/src/SignalR/common/Shared/ReusableUtf8JsonWriter.cs index 1dc980d750..c05c0397e6 100644 --- a/src/SignalR/common/Shared/ReusableUtf8JsonWriter.cs +++ b/src/SignalR/common/Shared/ReusableUtf8JsonWriter.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Text.Encodings.Web; using System.Text.Json; namespace Microsoft.AspNetCore.Internal @@ -20,7 +21,13 @@ namespace Microsoft.AspNetCore.Internal public ReusableUtf8JsonWriter(IBufferWriter<byte> stream) { - _writer = new Utf8JsonWriter(stream, new JsonWriterOptions() { SkipValidation = true }); + _writer = new Utf8JsonWriter(stream, new JsonWriterOptions() + { +#if !DEBUG + SkipValidation = true, +#endif + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); } public static ReusableUtf8JsonWriter Get(IBufferWriter<byte> stream) diff --git a/src/SignalR/common/SignalR.Common/src/Microsoft.AspNetCore.SignalR.Common.csproj b/src/SignalR/common/SignalR.Common/src/Microsoft.AspNetCore.SignalR.Common.csproj index fa97b3e082..eab77e2bf1 100644 --- a/src/SignalR/common/SignalR.Common/src/Microsoft.AspNetCore.SignalR.Common.csproj +++ b/src/SignalR/common/SignalR.Common/src/Microsoft.AspNetCore.SignalR.Common.csproj @@ -29,7 +29,7 @@ <Reference Include="System.Text.Json" /> </ItemGroup> - <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '3.1'"> + <ItemGroup Condition="'$(AspNetCoreMajorMinorVersion)' == '5.0'"> <!-- This dependency was replaced by System.Text.Json between 3.0 and 2.2. This suppression can be removed after 3.0 is complete. --> <SuppressBaselineReference Include="Newtonsoft.Json" /> diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/HandshakeProtocolTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/HandshakeProtocolTests.cs index 27560c502a..9a88d32a57 100644 --- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/HandshakeProtocolTests.cs +++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/HandshakeProtocolTests.cs @@ -15,6 +15,12 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol [InlineData("{\"protocol\":\"dummy\",\"version\":1}\u001e", "dummy", 1)] [InlineData("{\"protocol\":\"\",\"version\":10}\u001e", "", 10)] [InlineData("{\"protocol\":\"\",\"version\":10,\"unknown\":null}\u001e", "", 10)] + [InlineData("{\"protocol\":\"firstProtocol\",\"protocol\":\"secondProtocol\",\"version\":1}\u001e", "secondProtocol", 1)] + [InlineData("{\"protocol\":\"firstProtocol\",\"protocol\":\"secondProtocol\",\"version\":1,\"version\":75}\u001e", "secondProtocol", 75)] + [InlineData("{\"protocol\":\"dummy\",\"version\":1,\"ignoredField\":99}\u001e", "dummy", 1)] + [InlineData("{\"protocol\":\"dummy\",\"version\":1}{\"protocol\":\"wrong\",\"version\":99}\u001e", "dummy", 1)] + [InlineData("{\"protocol\":\"\\u0064ummy\",\"version\":1}\u001e", "dummy", 1)] + [InlineData("{\"\\u0070rotoco\\u006c\":\"\\u0064ummy\",\"version\":1}\u001e", "dummy", 1)] public void ParsingHandshakeRequestMessageSuccessForValidMessages(string json, string protocol, int version) { var message = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(json)); diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs index a2f696ab17..6f9ba5cb60 100644 --- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs +++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.SignalR.Protocol; @@ -28,7 +29,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol PayloadSerializerOptions = new JsonSerializerOptions() { IgnoreNullValues = ignoreNullValues, - PropertyNamingPolicy = useCamelCase ? JsonNamingPolicy.CamelCase : null + PropertyNamingPolicy = useCamelCase ? JsonNamingPolicy.CamelCase : null, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, } }; @@ -39,6 +41,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol [InlineData("", "Error reading JSON.")] [InlineData("42", "Unexpected JSON Token Type 'Number'. Expected a JSON Object.")] [InlineData("{\"type\":\"foo\"}", "Expected 'type' to be of type Number.")] + [InlineData("{\"type\":3,\"invocationId\":\"42\",\"result\":true", "Error reading JSON.")] public void CustomInvalidMessages(string input, string expectedMessage) { input = Frame(input); @@ -101,98 +104,18 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol Assert.Equal(expectedMessage, message); } - [Fact] - public void ReadCaseInsensitivePropertiesByDefault() - { - var input = Frame("{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StrIngProp\":\"test\",\"DoublePrOp\":3.14159,\"IntProp\":43,\"DateTimeProp\":\"2019-06-03T22:00:00\",\"NuLLProp\":null,\"ByteARRProp\":\"AgQG\"}}"); - - var binder = new TestBinder(null, typeof(TemporaryCustomObject)); - var data = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(input)); - JsonHubProtocol.TryParseMessage(ref data, binder, out var message); - - var streamItemMessage = Assert.IsType<StreamItemMessage>(message); - Assert.Equal(new TemporaryCustomObject() - { - ByteArrProp = new byte[] { 2, 4, 6 }, - IntProp = 43, - DoubleProp = 3.14159, - StringProp = "test", - DateTimeProp = DateTime.Parse("6/3/2019 10:00:00 PM") - }, streamItemMessage.Item); - } - public static IDictionary<string, JsonProtocolTestData> CustomProtocolTestData => new[] { new JsonProtocolTestData("InvocationMessage_HasFloatArgument", new InvocationMessage(null, "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"), - new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20\\u002B12:34\"]}"), - new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNoCamelCase", new InvocationMessage(null, "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), false, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), false, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), true, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), new JsonProtocolTestData("InvocationMessage_HasHeaders", AddHeaders(TestHeaders, new InvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f })), true, true, "{\"type\":1," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"), - new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNoCamelCase", new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), false, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnore", new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnoreAndNoCamelCase", new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), false, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), true, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), new JsonProtocolTestData("StreamItemMessage_HasFloatItem", new StreamItemMessage("123", 2.0f), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":2}"), - new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } })), true, false, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":2}"), - new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNoCamelCase", CompletionMessage.WithResult("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), false, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIgnore", CompletionMessage.WithResult("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIncludeAndNoCamelCase", CompletionMessage.WithResult("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), false, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueInclude", CompletionMessage.WithResult("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), true, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasErrorAndCamelCase", CompletionMessage.Empty("123"), true, true, "{\"type\":3,\"invocationId\":\"123\"}"), - - new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), false, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnore", new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), false, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueInclude", new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), true, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("StreamInvocationMessage_HasFloatArgument", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } })), true, false, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), }.ToDictionary(t => t.Name); public static IEnumerable<object[]> CustomProtocolTestDataNames => CustomProtocolTestData.Keys.Select(name => new object[] { name }); } - - // Revert back to CustomObject when initial values on arrays are supported - // e.g. byte[] arr { get; set; } = byte[] { 1, 2, 3 }; - internal class TemporaryCustomObject : IEquatable<TemporaryCustomObject> - { - // Not intended to be a full set of things, just a smattering of sample serializations - public string StringProp { get; set; } = "SignalR!"; - - public double DoubleProp { get; set; } = 6.2831853071; - - public int IntProp { get; set; } = 42; - - public DateTime DateTimeProp { get; set; } = new DateTime(2017, 4, 11, 0, 0, 0, DateTimeKind.Utc); - - public object NullProp { get; set; } = null; - - public byte[] ByteArrProp { get; set; } - - public override bool Equals(object obj) - { - return obj is TemporaryCustomObject o && Equals(o); - } - - public override int GetHashCode() - { - // This is never used in a hash table - return 0; - } - - public bool Equals(TemporaryCustomObject right) - { - // This allows the comparer below to properly compare the object in the test. - return string.Equals(StringProp, right.StringProp, StringComparison.Ordinal) && - DoubleProp == right.DoubleProp && - IntProp == right.IntProp && - DateTime.Equals(DateTimeProp, right.DateTimeProp) && - NullProp == right.NullProp && - System.Linq.Enumerable.SequenceEqual(ByteArrProp, right.ByteArrProp); - } - } } diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs index d0ef4a6e7f..1570b54462 100644 --- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs +++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs @@ -40,12 +40,30 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("InvocationMessage_HasStreamAndNormalArgument", new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[42],\"streamIds\":[\"__test_id__\"]}"), new JsonProtocolTestData("InvocationMessage_HasMultipleStreams", new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[],\"streamIds\":[\"__test_id__\",\"__test_id2__\"]}"), new JsonProtocolTestData("InvocationMessage_DateTimeOffsetArgument", new InvocationMessage("Method", new object[] { DateTimeOffset.Parse("2016-05-10T13:51:20+12:34") }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"), - + new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"), + new JsonProtocolTestData("InvocationMessage_HasNonAsciiArgument", new InvocationMessage("Method", new object[] { "מחרוזת כלשהי" }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"מחרוזת כלשהי\"]}"), + new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), + + new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, false, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnore", new StreamItemMessage("123", new CustomObject()), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnoreAndNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new CustomObject()), true, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), new JsonProtocolTestData("StreamItemMessage_HasIntegerItem", new StreamItemMessage("123", 1), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":1}"), new JsonProtocolTestData("StreamItemMessage_HasStringItem", new StreamItemMessage("123", "Foo"), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":\"Foo\"}"), new JsonProtocolTestData("StreamItemMessage_HasBoolItem", new StreamItemMessage("123", true), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":true}"), new JsonProtocolTestData("StreamItemMessage_HasNullItem", new StreamItemMessage("123", null), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":null}"), + new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIgnore", CompletionMessage.WithResult("123", new CustomObject()), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIncludeAndNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueInclude", CompletionMessage.WithResult("123", new CustomObject()), true, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("CompletionMessage_HasErrorAndCamelCase", CompletionMessage.Empty("123"), true, true, "{\"type\":3,\"invocationId\":\"123\"}"), + new JsonProtocolTestData("CompletionMessage_HasTestHeadersAndCustomItemResult", AddHeaders(TestHeaders, CompletionMessage.WithResult("123", new CustomObject())), true, false, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), + new JsonProtocolTestData("CompletionMessage_HasErrorAndHeadersAndCamelCase", AddHeaders(TestHeaders, CompletionMessage.Empty("123")), true, true, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\"}"), new JsonProtocolTestData("CompletionMessage_HasIntegerResult", CompletionMessage.WithResult("123", 1), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":1}"), new JsonProtocolTestData("CompletionMessage_HasStringResult", CompletionMessage.WithResult("123", "Foo"), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":\"Foo\"}"), new JsonProtocolTestData("CompletionMessage_HasBoolResult", CompletionMessage.WithResult("123", true), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":true}"), @@ -53,6 +71,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("CompletionMessage_HasError", CompletionMessage.WithError("123", "Whoops!"), true, true, "{\"type\":3,\"invocationId\":\"123\",\"error\":\"Whoops!\"}"), new JsonProtocolTestData("CompletionMessage_HasErrorAndHeaders", AddHeaders(TestHeaders, CompletionMessage.WithError("123", "Whoops!")), true, true, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"error\":\"Whoops!\"}"), + new JsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() })), true, false, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnore", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueInclude", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), new JsonProtocolTestData("StreamInvocationMessage_HasInvocationId", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo" }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\"]}"), new JsonProtocolTestData("StreamInvocationMessage_HasBoolArgument", new StreamInvocationMessage("123", "Target", new object[] { true }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[true]}"), new JsonProtocolTestData("StreamInvocationMessage_HasNullArgument", new StreamInvocationMessage("123", "Target", new object[] { null }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[null]}"), @@ -156,8 +179,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol [InlineData("{\"type\":4,\"invocationId\":\"42\",\"target\":\"foo\"}", "Missing required property 'arguments'.")] [InlineData("{\"type\":4,\"invocationId\":\"42\",\"target\":\"foo\",\"arguments\":{}}", "Expected 'arguments' to be of type Array.")] - //[InlineData("{\"type\":3,\"invocationId\":\"42\",\"error\":\"foo\",\"result\":true}", "The 'error' and 'result' properties are mutually exclusive.")] - //[InlineData("{\"type\":3,\"invocationId\":\"42\",\"result\":true", "Unexpected end when reading JSON.")] + [InlineData("{\"type\":3,\"invocationId\":\"42\",\"error\":\"foo\",\"result\":true}", "The 'error' and 'result' properties are mutually exclusive.")] public void InvalidMessages(string input, string expectedMessage) { input = Frame(input); @@ -270,6 +292,26 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol Assert.Equal("foo", bindingFailure.Target); } + [Fact] + public void ReadCaseInsensitivePropertiesByDefault() + { + var input = Frame("{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StrIngProp\":\"test\",\"DoublePrOp\":3.14159,\"IntProp\":43,\"DateTimeProp\":\"2019-06-03T22:00:00\",\"NuLLProp\":null,\"ByteARRProp\":\"AgQG\"}}"); + + var binder = new TestBinder(null, typeof(CustomObject)); + var data = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(input)); + JsonHubProtocol.TryParseMessage(ref data, binder, out var message); + + var streamItemMessage = Assert.IsType<StreamItemMessage>(message); + Assert.Equal(new CustomObject() + { + ByteArrProp = new byte[] { 2, 4, 6 }, + IntProp = 43, + DoubleProp = 3.14159, + StringProp = "test", + DateTimeProp = DateTime.Parse("6/3/2019 10:00:00 PM") + }, streamItemMessage.Item); + } + public static string Frame(string input) { var data = Encoding.UTF8.GetBytes(input); diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs index 255cdead83..de10b1dbf6 100644 --- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs +++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs @@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol [InlineData("", "Unexpected end when reading JSON.")] [InlineData("42", "Unexpected JSON Token Type 'Integer'. Expected a JSON Object.")] [InlineData("{\"type\":\"foo\"}", "Expected 'type' to be of type Integer.")] + [InlineData("{\"type\":3,\"invocationId\":\"42\",\"result\":true", "Unexpected end when reading JSON.")] public void CustomInvalidMessages(string input, string expectedMessage) { input = Frame(input); @@ -93,35 +94,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol public static IDictionary<string, JsonProtocolTestData> CustomProtocolTestData => new[] { new JsonProtocolTestData("InvocationMessage_HasFloatArgument", new InvocationMessage(null, "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"), - new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), false, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"), - new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), new JsonProtocolTestData("InvocationMessage_HasHeaders", AddHeaders(TestHeaders, new InvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f })), true, true, "{\"type\":1," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"), new JsonProtocolTestData("StreamItemMessage_HasFloatItem", new StreamItemMessage("123", 2.0f), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":2.0}"), - new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnore", new StreamItemMessage("123", new CustomObject()), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnoreAndNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new CustomObject()), true, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, false, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), - - new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIgnore", CompletionMessage.WithResult("123", new CustomObject()), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIncludeAndNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueInclude", CompletionMessage.WithResult("123", new CustomObject()), true, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasTestHeadersAndCustomItemResult", AddHeaders(TestHeaders, CompletionMessage.WithResult("123", new CustomObject())), true, false, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasErrorAndCamelCase", CompletionMessage.Empty("123"), true, true, "{\"type\":3,\"invocationId\":\"123\"}"), - new JsonProtocolTestData("CompletionMessage_HasErrorAndHeadersAndCamelCase", AddHeaders(TestHeaders, CompletionMessage.Empty("123")), true, true, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\"}"), + new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":2.0}"), new JsonProtocolTestData("StreamInvocationMessage_HasFloatArgument", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnore", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueInclude", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), - new JsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() })), true, false, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), }.ToDictionary(t => t.Name); public static IEnumerable<object[]> CustomProtocolTestDataNames => CustomProtocolTestData.Keys.Select(name => new object[] { name }); diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/Utf8BufferTextWriterTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/Utf8BufferTextWriterTests.cs index b1fe8c76fb..6b7a5563d3 100644 --- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/Utf8BufferTextWriterTests.cs +++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/Utf8BufferTextWriterTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -245,7 +245,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol } [Fact] - private void WriteMultiByteCharactersToSmallBuffers() + public void WriteMultiByteCharactersToSmallBuffers() { // Test string breakdown (char => UTF-8 hex values): // a => 61 diff --git a/src/SignalR/docs/specs/TransportProtocols.md b/src/SignalR/docs/specs/TransportProtocols.md index f11b80bc74..a4c10f4ead 100644 --- a/src/SignalR/docs/specs/TransportProtocols.md +++ b/src/SignalR/docs/specs/TransportProtocols.md @@ -12,19 +12,65 @@ A transport is required to have the following attributes: The only transport which fully implements the duplex requirement is WebSockets, the others are "half-transports" which implement one end of the duplex connection. They are used in combination to achieve a duplex connection. -Throughout this document, the term `[endpoint-base]` is used to refer to the route assigned to a particular end point. The term `[connection-id]` is used to refer to the connection ID provided by the `POST [endpoint-base]/negotiate` request. +Throughout this document, the term `[endpoint-base]` is used to refer to the route assigned to a particular end point. The terms `connection-id` and `connectionToken` are used to refer to the connection ID and connection token provided by the `POST [endpoint-base]/negotiate` request. **NOTE on errors:** In all error cases, by default, the detailed exception message is **never** provided; a short description string may be provided. However, an application developer may elect to allow detailed exception messages to be emitted, which should only be used in the `Development` environment. Unexpected errors are communicated by HTTP `500 Server Error` status codes or WebSockets non-`1000 Normal Closure` close frames; in these cases the connection should be considered to be terminated. ## `POST [endpoint-base]/negotiate` request -The `POST [endpoint-base]/negotiate` request is used to establish a connection between the client and the server. The content type of the response is `application/json`. The response to the `POST [endpoint-base]/negotiate` request contains one of three types of responses: +The `POST [endpoint-base]/negotiate` request is used to establish a connection between the client and the server. -1. A response that contains the `connectionId` which will be used to identify the connection on the server and the list of the transports supported by the server. +In the POST request the client sends a query string parameter with the key "negotiateVersion" and the value as the negotiate protocol version it would like to use. If the query string is omitted, the server treats the version as zero. The server will include a "negotiateVersion" property in the json response that says which version it will be using. The version is chosen as described below: +* If the servers minimum supported protocol version is greater than the version requested by the client it will send an error response and close the connection +* If the server supports the request version it will respond with the requested version +* If the requested version is greater than the servers largest supported version the server will respond with its largest supported version +The client may close the connection if the "negotiateVersion" in the response is not acceptable. +The content type of the response is `application/json` and is a JSON payload containing properties to assist the client in establishing a persistent connection. Extra JSON properties that the client does not know about should be ignored. This allows for future additions without breaking older clients. + +### Version 1 + +When the server and client agree on version 1 the server response will include a "connectionToken" property in addition to the "connectionId" property. The value of the "connectionToken" property will be used in the "id" query string for the HTTP requests described below, this value should be kept secret. + +A successful negotiate response will look similar to the following payload: ```json { + "connectionToken":"05265228-1e2c-46c5-82a1-6a5bcc3f0143", "connectionId":"807809a5-31bf-470d-9e23-afaee35d8a0d", + "negotiateVersion":1, + "availableTransports":[ + { + "transport": "WebSockets", + "transferFormats": [ "Text", "Binary" ] + }, + { + "transport": "ServerSentEvents", + "transferFormats": [ "Text" ] + }, + { + "transport": "LongPolling", + "transferFormats": [ "Text", "Binary" ] + } + ] + } + ``` + + The payload returned from this endpoint provides the following data: + + * The `connectionToken` which is **required** by the Long Polling and Server-Sent Events transports (in order to correlate sends and receives). + * The `connectionId` which is the id by which other clients can refer to it. + * The `negotiateVersion` which is the negotiation protocol version being used between the server and client. + * The `availableTransports` list which describes the transports the server supports. For each transport, the name of the transport (`transport`) is listed, as is a list of "transfer formats" supported by the transport (`transferFormats`) + +### Version 0 + +When the server and client agree on version 0 the server response will include a "connectionId" property that is used in the "id" query string for the HTTP requests described below. + +A successful negotiate response will look similar to the following payload: + ```json + { + "connectionId":"807809a5-31bf-470d-9e23-afaee35d8a0d", + "negotiateVersion":0, "availableTransports":[ { "transport": "WebSockets", @@ -45,10 +91,14 @@ The `POST [endpoint-base]/negotiate` request is used to establish a connection b The payload returned from this endpoint provides the following data: * The `connectionId` which is **required** by the Long Polling and Server-Sent Events transports (in order to correlate sends and receives). + * The `negotiateVersion` which is the negotiation protocol version being used between the server and client. * The `availableTransports` list which describes the transports the server supports. For each transport, the name of the transport (`transport`) is listed, as is a list of "transfer formats" supported by the transport (`transferFormats`) +### All versions + +There are two other possible negotiation responses: -2. A redirect response which tells the client which URL and optionally access token to use as a result. +1. A redirect response which tells the client which URL and optionally access token to use as a result. ```json { @@ -63,7 +113,7 @@ The `POST [endpoint-base]/negotiate` request is used to establish a connection b * The `accessToken` which is an optional bearer token for accessing the specified url. -3. A response that contains an `error` which should stop the connection attempt. +1. A response that contains an `error` which should stop the connection attempt. ```json { @@ -136,10 +186,14 @@ Long Polling requires that the client poll the server for new messages. Unlike t A Poll is established by sending an HTTP GET request to `[endpoint-base]` with the following query string parameters +#### Version 1 +* `id` (Required) - The Connection Token of the destination connection. + +#### Version 0 * `id` (Required) - The Connection ID of the destination connection. When data is available, the server responds with a body in one of the two formats below (depending upon the value of the `Accept` header). The response may be chunked, as per the chunked encoding part of the HTTP spec. If the `id` parameter is missing, a `400 Bad Request` response is returned. If there is no connection with the ID specified in `id`, a `404 Not Found` response is returned. -When the client has finished with the connection, it can issue a `DELETE` request to `[endpoint-base]` (with the `id` in the querystring) to gracefully terminate the connection. The server will complete the latest poll with `204` to indicate that it has shut down. +When the client has finished with the connection, it can issue a `DELETE` request to `[endpoint-base]` (with the `id` in the query string) to gracefully terminate the connection. The server will complete the latest poll with `204` to indicate that it has shut down. diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Commands/Defaults.cs b/src/SignalR/perf/benchmarkapps/Crankier/Commands/Defaults.cs index 732ccb6af3..84027fff16 100644 --- a/src/SignalR/perf/benchmarkapps/Crankier/Commands/Defaults.cs +++ b/src/SignalR/perf/benchmarkapps/Crankier/Commands/Defaults.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http.Connections; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.SignalR.Crankier.Commands { @@ -11,5 +12,6 @@ namespace Microsoft.AspNetCore.SignalR.Crankier.Commands public static readonly int NumberOfConnections = 10_000; public static readonly int SendDurationInSeconds = 300; public static readonly HttpTransportType TransportType = HttpTransportType.WebSockets; + public static readonly LogLevel LogLevel = LogLevel.None; } } diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Commands/ServerCommand.cs b/src/SignalR/perf/benchmarkapps/Crankier/Commands/ServerCommand.cs new file mode 100644 index 0000000000..cb1ef585b6 --- /dev/null +++ b/src/SignalR/perf/benchmarkapps/Crankier/Commands/ServerCommand.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.Extensions.CommandLineUtils; +using static Microsoft.AspNetCore.SignalR.Crankier.Commands.CommandLineUtilities; +using System.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.SignalR.Crankier.Server; + +namespace Microsoft.AspNetCore.SignalR.Crankier.Commands +{ + internal class ServerCommand + { + public static void Register(CommandLineApplication app) + { + app.Command("server", cmd => + { + var logLevelOption = cmd.Option("--log <LOG_LEVEL>", "The LogLevel to use.", CommandOptionType.SingleValue); + + cmd.OnExecute(() => + { + LogLevel logLevel = Defaults.LogLevel; + + if (logLevelOption.HasValue() && !Enum.TryParse(logLevelOption.Value(), out logLevel)) + { + return InvalidArg(logLevelOption); + } + return Execute(logLevel); + }); + }); + } + + private static int Execute(LogLevel logLevel) + { + Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}"); + + var config = new ConfigurationBuilder() + .AddEnvironmentVariables(prefix: "ASPNETCORE_") + .Build(); + + var host = new WebHostBuilder() + .UseConfiguration(config) + .ConfigureLogging(loggerFactory => + { + loggerFactory.AddConsole().SetMinimumLevel(logLevel); + }) + .UseKestrel() + .UseStartup<Startup>(); + + host.Build().Run(); + + return 0; + } + } +} diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Crankier.csproj b/src/SignalR/perf/benchmarkapps/Crankier/Crankier.csproj index 1c1b18a058..1bc1e98bd6 100644 --- a/src/SignalR/perf/benchmarkapps/Crankier/Crankier.csproj +++ b/src/SignalR/perf/benchmarkapps/Crankier/Crankier.csproj @@ -9,6 +9,11 @@ <ItemGroup> <Reference Include="Microsoft.AspNetCore.SignalR.Client" /> <Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" /> + <Reference Include="Microsoft.AspNetCore.SignalR" /> + <Reference Include="Microsoft.AspNetCore.Server.Kestrel" /> + <Reference Include="Microsoft.Extensions.Configuration.CommandLine" /> + <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" /> + <Reference Include="Microsoft.Extensions.Logging.Console" /> <Reference Include="Newtonsoft.Json" /> </ItemGroup> diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Program.cs b/src/SignalR/perf/benchmarkapps/Crankier/Program.cs index a3443ecd94..93f53ea328 100644 --- a/src/SignalR/perf/benchmarkapps/Crankier/Program.cs +++ b/src/SignalR/perf/benchmarkapps/Crankier/Program.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.SignalR.Crankier LocalCommand.Register(app); AgentCommand.Register(app); WorkerCommand.Register(app); + ServerCommand.Register(app); app.Command("help", cmd => { diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Readme.md b/src/SignalR/perf/benchmarkapps/Crankier/Readme.md index 20f56cc720..7b0a95fbe9 100644 --- a/src/SignalR/perf/benchmarkapps/Crankier/Readme.md +++ b/src/SignalR/perf/benchmarkapps/Crankier/Readme.md @@ -4,6 +4,24 @@ Load testing for ASP.NET Core SignalR ## Commands +### server + +The `server` command runs a web host exposing a single SignalR `Hub` endpoint on `/echo`. After the first client connection, the server will periodically write concurrent connection information to the console. + +``` +> dotnet run -- help server + +Usage: server [options] + +Options: + --log <LOG_LEVEL> The LogLevel to use. +``` + +Notes: + +* `LOG_LEVEL` switches internal logging only, not concurrent connection information, and defaults to `LogLevel.None`. Use this option to control Kestrel / SignalR Warnings & Errors being logged to console. + + ### local The `local` command launches a set of local worker clients to establish connections to your SignalR server. @@ -31,13 +49,19 @@ Notes: #### Examples -Attempt to make 10,000 connections to the `echo` hub using WebSockets and 10 workers: +Run the server: + +``` +dotnet run -- server +``` + +Attempt to make 10,000 connections to the server using WebSockets and 10 workers: ``` dotnet run -- local --target-url https://localhost:5001/echo --workers 10 ``` -Attempt to make 5,000 connections to the `echo` hub using Long Polling +Attempt to make 5,000 connections to the server using Long Polling ``` dotnet run -- local --target-url https://localhost:5001/echo --connections 5000 --transport LongPolling diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounter.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounter.cs new file mode 100644 index 0000000000..1ab6a25abc --- /dev/null +++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounter.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.SignalR.Crankier.Server +{ + public class ConnectionCounter + { + private int _totalConnectedCount; + private int _peakConnectedCount; + private int _totalDisconnectedCount; + private int _receivedCount; + + private readonly object _lock = new object(); + + public ConnectionSummary Summary + { + get + { + lock (_lock) + { + return new ConnectionSummary + { + CurrentConnections = _totalConnectedCount - _totalDisconnectedCount, + PeakConnections = _peakConnectedCount, + TotalConnected = _totalConnectedCount, + TotalDisconnected = _totalDisconnectedCount, + ReceivedCount = _receivedCount + }; + } + } + } + + public void Receive(string payload) + { + lock (_lock) + { + _receivedCount += payload.Length; + } + } + + public void Connected() + { + lock (_lock) + { + _totalConnectedCount++; + _peakConnectedCount = Math.Max(_totalConnectedCount - _totalDisconnectedCount, _peakConnectedCount); + } + } + + public void Disconnected() + { + lock (_lock) + { + _totalDisconnectedCount++; + } + } + } +}
\ No newline at end of file diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounterHostedService.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounterHostedService.cs new file mode 100644 index 0000000000..44b8bb26f2 --- /dev/null +++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounterHostedService.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.SignalR.Crankier.Server +{ + public class ConnectionCounterHostedService : IHostedService, IDisposable + { + private Stopwatch _timeSinceFirstConnection; + private readonly ConnectionCounter _counter; + private ConnectionSummary _lastSummary; + private Timer _timer; + private int _executingDoWork; + + public ConnectionCounterHostedService(ConnectionCounter counter) + { + _counter = counter; + _timeSinceFirstConnection = new Stopwatch(); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + + return Task.CompletedTask; + } + + private void DoWork(object state) + { + if (Interlocked.Exchange(ref _executingDoWork, 1) == 0) + { + var summary = _counter.Summary; + + if (summary.PeakConnections > 0) + { + if (_timeSinceFirstConnection.ElapsedTicks == 0) + { + _timeSinceFirstConnection.Start(); + } + + var elapsed = _timeSinceFirstConnection.Elapsed; + + if (_lastSummary != null) + { + Console.WriteLine(@"[{0:hh\:mm\:ss}] Current: {1}, peak: {2}, connected: {3}, disconnected: {4}, rate: {5}/s", + elapsed, + summary.CurrentConnections, + summary.PeakConnections, + summary.TotalConnected - _lastSummary.TotalConnected, + summary.TotalDisconnected - _lastSummary.TotalDisconnected, + summary.CurrentConnections - _lastSummary.CurrentConnections + ); + } + + _lastSummary = summary; + } + + Interlocked.Exchange(ref _executingDoWork, 0); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } + } +}
\ No newline at end of file diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionSummary.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionSummary.cs new file mode 100644 index 0000000000..83f38aaf62 --- /dev/null +++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionSummary.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.SignalR.Crankier.Server +{ + public class ConnectionSummary + { + public int TotalConnected { get; set; } + + public int TotalDisconnected { get; set; } + + public int PeakConnections { get; set; } + + public int CurrentConnections { get; set; } + + public int ReceivedCount { get; set; } + } +}
\ No newline at end of file diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/EchoHub.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/EchoHub.cs new file mode 100644 index 0000000000..0b24b46e9b --- /dev/null +++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/EchoHub.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; + +namespace Microsoft.AspNetCore.SignalR.Crankier.Server +{ + public class EchoHub : Hub + { + private ConnectionCounter _counter; + + public EchoHub(ConnectionCounter counter) + { + _counter = counter; + } + + public async Task Broadcast(int duration) + { + var sent = 0; + try + { + var t = new CancellationTokenSource(); + t.CancelAfter(TimeSpan.FromSeconds(duration)); + while (!t.IsCancellationRequested && !Context.ConnectionAborted.IsCancellationRequested) + { + await Clients.All.SendAsync("send", DateTime.UtcNow); + sent++; + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + Console.WriteLine("Broadcast exited: Sent {0} messages", sent); + } + + public override Task OnConnectedAsync() + { + _counter?.Connected(); + return Task.CompletedTask; + } + + public override Task OnDisconnectedAsync(Exception exception) + { + _counter?.Disconnected(); + return Task.CompletedTask; + } + + public DateTime Echo(DateTime time) + { + return time; + } + + public Task EchoAll(DateTime time) + { + return Clients.All.SendAsync("send", time); + } + + public void SendPayload(string payload) + { + _counter?.Receive(payload); + } + + public DateTime GetCurrentTime() + { + return DateTime.UtcNow; + } + } +} diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/Startup.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/Startup.cs new file mode 100644 index 0000000000..d8d5efc5bf --- /dev/null +++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/Startup.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.SignalR.Crankier.Server +{ + public class Startup + { + private readonly IConfiguration _config; + public Startup(IConfiguration configuration) + { + _config = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + var signalrBuilder = services.AddSignalR() + .AddMessagePackProtocol(); + + services.AddSingleton<ConnectionCounter>(); + + services.AddHostedService<ConnectionCounterHostedService>(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapHub<EchoHub>("/echo"); + }); + } + } +} diff --git a/src/SignalR/publish-apps.ps1 b/src/SignalR/publish-apps.ps1 index fcb8c99cae..8c7b2de4a4 100644 --- a/src/SignalR/publish-apps.ps1 +++ b/src/SignalR/publish-apps.ps1 @@ -1,4 +1,4 @@ -param($RootDirectory = (Get-Location), $Framework = "netcoreapp3.1", $Runtime = "win-x64", $CommitHash, $BranchName, $BuildNumber) +param($RootDirectory = (Get-Location), $Framework = "netcoreapp5.0", $Runtime = "win-x64", $CommitHash, $BranchName, $BuildNumber) # De-Powershell the path $RootDirectory = (Convert-Path $RootDirectory) diff --git a/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs b/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs index 3c835ab933..a8a9fe2095 100644 --- a/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs +++ b/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs @@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.SignalR return SendToAllConnections(methodName, args, null); } - private Task SendToAllConnections(string methodName, object[] args, Func<HubConnectionContext, bool> include) + private Task SendToAllConnections(string methodName, object[] args, Func<HubConnectionContext, object, bool> include, object state = null) { List<Task> tasks = null; SerializedHubMessage message = null; @@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.SignalR // foreach over HubConnectionStore avoids allocating an enumerator foreach (var connection in _connections) { - if (include != null && !include(connection)) + if (include != null && !include(connection, state)) { continue; } @@ -127,12 +127,12 @@ namespace Microsoft.AspNetCore.SignalR // Tasks and message are passed by ref so they can be lazily created inside the method post-filtering, // while still being re-usable when sending to multiple groups - private void SendToGroupConnections(string methodName, object[] args, ConcurrentDictionary<string, HubConnectionContext> connections, Func<HubConnectionContext, bool> include, ref List<Task> tasks, ref SerializedHubMessage message) + private void SendToGroupConnections(string methodName, object[] args, ConcurrentDictionary<string, HubConnectionContext> connections, Func<HubConnectionContext, object, bool> include, object state, ref List<Task> tasks, ref SerializedHubMessage message) { // foreach over ConcurrentDictionary avoids allocating an enumerator foreach (var connection in connections) { - if (include != null && !include(connection.Value)) + if (include != null && !include(connection.Value, state)) { continue; } @@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.SignalR // group might be modified inbetween checking and sending List<Task> tasks = null; SerializedHubMessage message = null; - SendToGroupConnections(methodName, args, group, null, ref tasks, ref message); + SendToGroupConnections(methodName, args, group, null, null, ref tasks, ref message); if (tasks != null) { @@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.SignalR var group = _groups[groupName]; if (group != null) { - SendToGroupConnections(methodName, args, group, null, ref tasks, ref message); + SendToGroupConnections(methodName, args, group, null, null, ref tasks, ref message); } } @@ -247,7 +247,7 @@ namespace Microsoft.AspNetCore.SignalR List<Task> tasks = null; SerializedHubMessage message = null; - SendToGroupConnections(methodName, args, group, connection => !excludedConnectionIds.Contains(connection.ConnectionId), ref tasks, ref message); + SendToGroupConnections(methodName, args, group, (connection, state) => !((IReadOnlyList<string>)state).Contains(connection.ConnectionId), excludedConnectionIds, ref tasks, ref message); if (tasks != null) { @@ -271,7 +271,7 @@ namespace Microsoft.AspNetCore.SignalR /// <inheritdoc /> public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, connection => string.Equals(connection.UserIdentifier, userId, StringComparison.Ordinal)); + return SendToAllConnections(methodName, args, (connection, state) => string.Equals(connection.UserIdentifier, (string)state, StringComparison.Ordinal), userId); } /// <inheritdoc /> @@ -292,19 +292,19 @@ namespace Microsoft.AspNetCore.SignalR /// <inheritdoc /> public override Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, connection => !excludedConnectionIds.Contains(connection.ConnectionId)); + return SendToAllConnections(methodName, args, (connection, state) => !((IReadOnlyList<string>)state).Contains(connection.ConnectionId), excludedConnectionIds); } /// <inheritdoc /> public override Task SendConnectionsAsync(IReadOnlyList<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, connection => connectionIds.Contains(connection.ConnectionId)); + return SendToAllConnections(methodName, args, (connection, state) => ((IReadOnlyList<string>)state).Contains(connection.ConnectionId), connectionIds); } /// <inheritdoc /> public override Task SendUsersAsync(IReadOnlyList<string> userIds, string methodName, object[] args, CancellationToken cancellationToken = default) { - return SendToAllConnections(methodName, args, connection => userIds.Contains(connection.UserIdentifier)); + return SendToAllConnections(methodName, args, (connection, state) => ((IReadOnlyList<string>)state).Contains(connection.UserIdentifier), userIds); } } } diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs index 0b7bd620ec..bc0f8c9fc5 100644 --- a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs +++ b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs @@ -276,7 +276,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal { if (descriptor.OriginalParameterTypes[parameterPointer] == typeof(CancellationToken)) { - cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted); + cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted, default); arguments[parameterPointer] = cts.Token; } else if (isStreamCall && ReflectionHelper.IsStreamingType(descriptor.OriginalParameterTypes[parameterPointer], mustBeDirectType: true)) @@ -309,7 +309,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal return; } - cts = cts ?? CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted); + cts = cts ?? CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted, default); connection.ActiveRequestCancellationSources.TryAdd(hubMethodInvocationMessage.InvocationId, cts); var enumerable = descriptor.FromReturnedStream(result, cts.Token); diff --git a/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs b/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs index 511280d636..a333c600a8 100644 --- a/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs +++ b/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs @@ -132,6 +132,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal methodBuilder.DefineGenericParameters(genericTypeNames); } + // Check to see if the last parameter of the method is a CancellationToken + bool hasCancellationToken = paramTypes.LastOrDefault() == typeof(CancellationToken); + if (hasCancellationToken) + { + // remove CancellationToken from input paramTypes + paramTypes = paramTypes.Take(paramTypes.Length - 1).ToArray(); + } + var generator = methodBuilder.GetILGenerator(); // Declare local variable to store the arguments to IClientProxy.SendCoreAsync @@ -145,7 +153,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal generator.Emit(OpCodes.Ldstr, interfaceMethodInfo.Name); // Create an new object array to hold all the parameters to this method - generator.Emit(OpCodes.Ldc_I4, parameters.Length); // Stack: + generator.Emit(OpCodes.Ldc_I4, paramTypes.Length); // Stack: generator.Emit(OpCodes.Newarr, typeof(object)); // allocate object array generator.Emit(OpCodes.Stloc_0); @@ -162,8 +170,16 @@ namespace Microsoft.AspNetCore.SignalR.Internal // Load parameter array on to the stack. generator.Emit(OpCodes.Ldloc_0); - // Get 'CancellationToken.None' and put it on the stack, since we don't support CancellationToken right now - generator.Emit(OpCodes.Call, CancellationTokenNoneProperty.GetMethod); + if (hasCancellationToken) + { + // Get CancellationToken from input argument and put it on the stack + generator.Emit(OpCodes.Ldarg, paramTypes.Length + 1); + } + else + { + // Get 'CancellationToken.None' and put it on the stack, for when method does not have CancellationToken + generator.Emit(OpCodes.Call, CancellationTokenNoneProperty.GetMethod); + } // Send! generator.Emit(OpCodes.Callvirt, invokeMethod); diff --git a/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs b/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs index 4f68f6fe74..f88820cb78 100644 --- a/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs +++ b/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs @@ -76,6 +76,41 @@ namespace Microsoft.AspNetCore.SignalR.Tests.Internal } [Fact] + public async Task SupportsCancellationToken() + { + var clientProxy = new MockProxy(); + var typedProxy = TypedClientBuilder<ICancellationTokenMethod>.Build(clientProxy); + CancellationTokenSource cts1 = new CancellationTokenSource(); + var task1 = typedProxy.Method("foo", cts1.Token); + Assert.False(task1.IsCompleted); + + CancellationTokenSource cts2 = new CancellationTokenSource(); + var task2 = typedProxy.NoArgumentMethod(cts2.Token); + Assert.False(task2.IsCompleted); + + Assert.Collection(clientProxy.Sends, + send1 => + { + Assert.Equal("Method", send1.Method); + Assert.Single(send1.Arguments); + Assert.Collection(send1.Arguments, + arg1 => Assert.Equal("foo", arg1)); + Assert.Equal(cts1.Token, send1.CancellationToken); + send1.Complete(); + }, + send2 => + { + Assert.Equal("NoArgumentMethod", send2.Method); + Assert.Empty(send2.Arguments); + Assert.Equal(cts2.Token, send2.CancellationToken); + send2.Complete(); + }); + + await task1.OrTimeout(); + await task2.OrTimeout(); + } + + [Fact] public void ThrowsIfProvidedAClass() { var clientProxy = new MockProxy(); @@ -179,6 +214,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests.Internal Task SubMethod(string foo); } + public interface ICancellationTokenMethod + { + Task Method(string foo, CancellationToken cancellationToken); + Task NoArgumentMethod(CancellationToken cancellationToken); + } + public interface IPropertiesClient { string Property { get; } diff --git a/src/SignalR/server/SignalR/test/UserAgentHeaderTest.cs b/src/SignalR/server/SignalR/test/UserAgentHeaderTest.cs new file mode 100644 index 0000000000..6c3ba450d8 --- /dev/null +++ b/src/SignalR/server/SignalR/test/UserAgentHeaderTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Http.Connections.Client; +using Xunit; +using Constants = Microsoft.AspNetCore.Http.Connections.Client.Internal.Constants; + +namespace Microsoft.AspNetCore.Http.Connections.Tests +{ + public class UserAgentHeaderTest + { + [Fact] + public void UserAgentHeaderIsAccurate() + { + var majorVersion = typeof(HttpConnection).Assembly.GetName().Version.Major; + var minorVersion = typeof(HttpConnection).Assembly.GetName().Version.Minor; + var version = typeof(HttpConnection).Assembly.GetName().Version; + var os = RuntimeInformation.OSDescription; + var runtime = ".NET"; + var runtimeVersion = RuntimeInformation.FrameworkDescription; + var assemblyVersion = typeof(Constants) + .Assembly + .GetCustomAttributes<AssemblyInformationalVersionAttribute>() + .FirstOrDefault(); + var userAgent = Constants.UserAgentHeader; + var expectedUserAgent = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; {os}; {runtime}; {runtimeVersion})"; + + Assert.Equal(expectedUserAgent, userAgent); + } + } +} diff --git a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisProtocol.cs b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisProtocol.cs index a1594b0fd3..b6f276ab5e 100644 --- a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisProtocol.cs +++ b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisProtocol.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Internal // * Acks are sent to the Acknowledgement channel. // * See the Write[type] methods for a description of the protocol for each in-depth. // * The "Variable length integer" is the length-prefixing format used by BinaryReader/BinaryWriter: - // * https://docs.microsoft.com/en-us/dotnet/api/system.io.binarywriter.write?view=netstandard-2.0 + // * https://docs.microsoft.com/dotnet/api/system.io.binarywriter.write?view=netcore-2.2 // * The "Length prefixed string" is the string format used by BinaryReader/BinaryWriter: // * A 7-bit variable length integer encodes the length in bytes, followed by the encoded string in UTF-8. diff --git a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj index 4ef7283db6..82e665d1c0 100644 --- a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj +++ b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj @@ -24,6 +24,8 @@ <ItemGroup> <Reference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.2.1" Version="$(MicrosoftAspNetCoreAzureAppServicesSiteExtension21PackageVersion)" PrivateAssets="All" /> <Reference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.2.2" Version="$(MicrosoftAspNetCoreAzureAppServicesSiteExtension22PackageVersion)" PrivateAssets="All" /> + <PackageReference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.5.0.x86" Version="$(PackageVersion)" PrivateAssets="All" /> + <PackageReference Include="Microsoft.AspNetCore.AzureAppServices.SiteExtension.5.0.x64" Version="$(PackageVersion)" PrivateAssets="All" /> </ItemGroup> <ItemGroup> diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs index d230e1bb8d..7e33386d27 100644 --- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs +++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs @@ -21,8 +21,7 @@ namespace Microsoft.DotNet.OpenApi.Tests protected readonly TextWriter _output = new StringWriter(); protected readonly TextWriter _error = new StringWriter(); protected readonly ITestOutputHelper _outputHelper; - protected const string TestTFM = "netcoreapp3.1"; - + protected const string TestTFM = "netcoreapp5.0"; protected const string Content = @"{""x-generator"": ""NSwag""}"; protected const string ActualUrl = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/api-with-examples.yaml"; diff --git a/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs b/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs index adcbe32b1e..5a42903675 100644 --- a/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs +++ b/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs @@ -35,7 +35,7 @@ namespace Microsoft.Extensions.Configuration.UserSecrets.Tests private const string ProjectTemplate = @"<Project ToolsVersion=""15.0"" Sdk=""Microsoft.NET.Sdk""> <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFrameworks>netcoreapp3.1</TargetFrameworks> + <TargetFrameworks>netcoreapp5.0</TargetFrameworks> {0} <EnableDefaultCompileItems>false</EnableDefaultCompileItems> </PropertyGroup> diff --git a/src/Tools/dotnet-watch/test/ProgramTests.cs b/src/Tools/dotnet-watch/test/ProgramTests.cs index 0e7dff9b82..6e24eb13d2 100644 --- a/src/Tools/dotnet-watch/test/ProgramTests.cs +++ b/src/Tools/dotnet-watch/test/ProgramTests.cs @@ -28,7 +28,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests { _tempDir .WithCSharpProject("testproj") - .WithTargetFrameworks("netcoreapp3.1") + .WithTargetFrameworks("netcoreapp5.0") .Dir() .WithFile("Program.cs") .Create(); diff --git a/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj b/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj index d0cde953c7..7399c1018d 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj +++ b/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>netcoreapp5.0</TargetFramework> <OutputType>exe</OutputType> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> diff --git a/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj b/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj index 9f015b1ee4..8f8043d0de 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj +++ b/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>netcoreapp5.0</TargetFramework> <OutputType>exe</OutputType> <EnableDefaultCompileItems>false</EnableDefaultCompileItems> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj b/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj index af6de1b33f..6de103d382 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj +++ b/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj @@ -9,7 +9,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>netcoreapp5.0</TargetFramework> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> diff --git a/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj b/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj index 95412443e6..110ff7686b 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj +++ b/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netcoreapp3.1</TargetFramework> + <TargetFramework>netcoreapp5.0</TargetFramework> <OutputType>exe</OutputType> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> |