Merge branch 'merge/release/3.1-to-master' of https://github.com/dotnet-maestro-bot/AspNetCore

This commit is contained in:
Brennan Conroy 2019-10-31 14:36:13 -07:00
commit 764e4786e8
66 changed files with 8815 additions and 5228 deletions

View File

@ -65,7 +65,10 @@ variables:
valule: test
- name: _PublishArgs
value: ''
# used for post-build phases, internal builds only
- ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:
- group: DotNet-AspNet-SDLValidation-Params
stages:
- stage: build
displayName: Build
@ -654,3 +657,17 @@ stages:
enableSymbolValidation: false
enableSigningValidation: false
publishInstallersAndChecksums: true
# This is to enable SDL runs part of Post-Build Validation Stage
SDLValidationParameters:
enable: true
continueOnError: false
params: ' -SourceToolsList @("policheck","credscan")
-TsaInstanceURL $(_TsaInstanceURL)
-TsaProjectName $(_TsaProjectName)
-TsaNotificationEmail $(_TsaNotificationEmail)
-TsaCodebaseAdmin $(_TsaCodebaseAdmin)
-TsaBugAreaPath $(_TsaBugAreaPath)
-TsaIterationPath $(_TsaIterationPath)
-TsaRepositoryName "AspNetCore"
-TsaCodebaseName "AspNetCore"
-TsaPublish $True'

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
@ -27,6 +28,7 @@ namespace Microsoft.AspNetCore.Analyzers
{
MiddlewareItem? useAuthorizationItem = default;
MiddlewareItem? useRoutingItem = default;
MiddlewareItem? useEndpoint = default;
var length = middlewareAnalysis.Middleware.Length;
for (var i = length - 1; i >= 0; i-- )
@ -70,9 +72,24 @@ namespace Microsoft.AspNetCore.Analyzers
useAuthorizationItem.Operation.Syntax.GetLocation(),
middlewareItem.UseMethod.Name));
}
useEndpoint = middlewareItem;
}
else if (middleware == "UseRouting")
{
if (useEndpoint is null)
{
// We're likely here because the middleware uses an expression chain e.g.
// app.UseRouting()
// .UseAuthorization()
// .UseEndpoints(..));
// This analyzer expects MiddlewareItem instances to appear in the order in which they appear in source
// which unfortunately isn't true for chained calls (the operations appear in reverse order).
// We'll avoid doing any analysis in this event and rely on the runtime guardrails.
// We'll use https://github.com/aspnet/AspNetCore/issues/16648 to track addressing this in a future milestone
return;
}
useRoutingItem = middlewareItem;
}
}

View File

@ -244,6 +244,22 @@ namespace Microsoft.AspNetCore.Analyzers
Assert.Empty(diagnostics);
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredAsAChain_ReportsNoDiagnostics()
{
// Regression test for https://github.com/aspnet/AspNetCore/issues/15203
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthConfiguredCorrectlyChained));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationInvokedMultipleTimesInEndpointRoutingBlock_ReportsNoDiagnostics()
{
@ -279,6 +295,23 @@ namespace Microsoft.AspNetCore.Analyzers
});
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredBeforeUseRoutingChained_ReportsDiagnostics()
{
// This one asserts a false negative for https://github.com/aspnet/AspNetCore/issues/15203.
// We don't correctly identify chained calls, this test verifies the behavior.
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthBeforeUseRoutingChained));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredAfterUseEndpoints_ReportsDiagnostics()
{

View File

@ -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 Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest {
public class UseAuthBeforeUseRoutingChained
{
public void Configure(IApplicationBuilder app)
{
app.UseFileServer()
.UseAuthorization()
.UseRouting()
.UseEndpoints(r => { });
}
}
}

View File

@ -0,0 +1,16 @@
// 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;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest {
public class UseAuthConfiguredCorrectlyChained
{
public void Configure(IApplicationBuilder app)
{
app.UseRouting()
.UseAuthorization()
.UseEndpoints(r => { });
}
}
}

View File

@ -54,13 +54,9 @@ namespace Microsoft.AspNetCore.Blazor.Http
SameOrigin = 1,
Include = 2,
}
public partial class WebAssemblyHttpMessageHandler : System.Net.Http.HttpMessageHandler
public static partial class WebAssemblyHttpMessageHandlerOptions
{
public const string FetchArgs = "WebAssemblyHttpMessageHandler.FetchArgs";
public WebAssemblyHttpMessageHandler() { }
public static Microsoft.AspNetCore.Blazor.Http.FetchCredentialsOption DefaultCredentials { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[System.Diagnostics.DebuggerStepThroughAttribute]
protected override System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; }
public static Microsoft.AspNetCore.Blazor.Http.FetchCredentialsOption DefaultCredentials { get { throw null; } set { } }
}
}
namespace Microsoft.AspNetCore.Blazor.Rendering

View File

@ -2,11 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Http;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
@ -30,16 +27,6 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
public Task StartAsync(CancellationToken cancellationToken = default)
{
// We need to do this as early as possible, it eliminates a bunch of problems. Note that what we do
// is a bit fragile. If you see things breaking because JSRuntime.Current isn't set, then it's likely
// that something on the startup path went wrong.
//
// We want to the JSRuntime created here to be the 'ambient' runtime when JS calls back into .NET. When
// this happens in the browser it will be a direct call from Mono. We effectively needs to set the
// JSRuntime in the 'root' execution context which implies that we want to do as part of a direct
// call from Program.Main, and before any 'awaits'.
SetBrowserHttpMessageHandlerAsDefault();
return StartAsyncAwaited();
}
@ -102,35 +89,5 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
{
(Services as IDisposable)?.Dispose();
}
private static void SetBrowserHttpMessageHandlerAsDefault()
{
// Within the Mono WebAssembly BCL, this is a special private static field
// that can be assigned to override the default handler
const string getHttpMessageHandlerFieldName = "GetHttpMessageHandler";
var getHttpMessageHandlerField = typeof(HttpClient).GetField(
getHttpMessageHandlerFieldName,
BindingFlags.Static | BindingFlags.NonPublic);
// getHttpMessageHandlerField will be null in tests, but nonnull when actually
// running under Mono WebAssembly
if (getHttpMessageHandlerField != null)
{
// Just in case you're not actually using HttpClient, defer the construction
// of the WebAssemblyHttpMessageHandler
var handlerSingleton = new Lazy<HttpMessageHandler>(
() => new WebAssemblyHttpMessageHandler());
Func<HttpMessageHandler> handlerFactory = () => handlerSingleton.Value;
getHttpMessageHandlerField.SetValue(null, handlerFactory);
}
else
{
// We log a warning in case this ever happens at runtime (even though there's
// no obvious way it could be possible), but don't actually throw because that
// would break unit tests
Console.WriteLine("WARNING: Could not set default HttpMessageHandler because " +
$"'{getHttpMessageHandlerFieldName}' was not found on '{typeof(HttpClient).FullName}'.");
}
}
}
}

View File

@ -1,204 +0,0 @@
// 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;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Blazor.Http
{
/// <summary>
/// A browser-compatible implementation of <see cref="HttpMessageHandler"/>
/// </summary>
public class WebAssemblyHttpMessageHandler : HttpMessageHandler
{
/// <summary>
/// Gets or sets the default value of the 'credentials' option on outbound HTTP requests.
/// Defaults to <see cref="FetchCredentialsOption.SameOrigin"/>.
/// </summary>
public static FetchCredentialsOption DefaultCredentials { get; set; }
= FetchCredentialsOption.SameOrigin;
private static readonly object _idLock = new object();
private static readonly IDictionary<int, TaskCompletionSource<HttpResponseMessage>> _pendingRequests
= new Dictionary<int, TaskCompletionSource<HttpResponseMessage>>();
private static int _nextRequestId = 0;
/// <summary>
/// The name of a well-known property that can be added to <see cref="HttpRequestMessage.Properties"/>
/// to control the arguments passed to the underlying JavaScript <code>fetch</code> API.
/// </summary>
public const string FetchArgs = "WebAssemblyHttpMessageHandler.FetchArgs";
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<HttpResponseMessage>();
cancellationToken.Register(() => tcs.TrySetCanceled());
int id;
lock (_idLock)
{
id = _nextRequestId++;
_pendingRequests.Add(id, tcs);
}
var options = new FetchOptions();
if (request.Properties.TryGetValue(FetchArgs, out var fetchArgs))
{
options.RequestInitOverrides = fetchArgs;
}
options.RequestInit = new RequestInit
{
Credentials = GetDefaultCredentialsString(),
Headers = GetHeaders(request),
Method = request.Method.Method
};
options.RequestUri = request.RequestUri.ToString();
WebAssemblyJSRuntime.Instance.InvokeUnmarshalled<int, byte[], string, object>(
"Blazor._internal.http.sendAsync",
id,
request.Content == null ? null : await request.Content.ReadAsByteArrayAsync(),
JsonSerializer.Serialize(options, JsonSerializerOptionsProvider.Options));
return await tcs.Task;
}
/// <remarks>
/// While it may be tempting to remove this method because it appears to be unused,
/// this method is referenced by client code and must persist.
/// </remarks>
#pragma warning disable IDE0051 // Remove unused private members
private static void ReceiveResponse(
#pragma warning restore IDE0051 // Remove unused private members
string id,
string responseDescriptorJson,
byte[] responseBodyData,
string errorText)
{
TaskCompletionSource<HttpResponseMessage> tcs;
var idVal = int.Parse(id);
lock (_idLock)
{
tcs = _pendingRequests[idVal];
_pendingRequests.Remove(idVal);
}
if (errorText != null)
{
tcs.SetException(new HttpRequestException(errorText));
}
else
{
var responseDescriptor = JsonSerializer.Deserialize<ResponseDescriptor>(responseDescriptorJson, JsonSerializerOptionsProvider.Options);
var responseContent = responseBodyData == null ? null : new ByteArrayContent(responseBodyData);
var responseMessage = responseDescriptor.ToResponseMessage(responseContent);
tcs.SetResult(responseMessage);
}
}
/// <remarks>
/// While it may be tempting to remove this method because it appears to be unused,
/// this method is referenced by client code and must persist.
/// </remarks>
#pragma warning disable IDE0051 // Remove unused private members
private static byte[] AllocateArray(string length) => new byte[int.Parse(length)];
#pragma warning restore IDE0051 // Remove unused private members
private static IReadOnlyList<Header> GetHeaders(HttpRequestMessage request)
{
var requestHeaders = request.Headers.AsEnumerable();
if (request.Content?.Headers != null)
{
requestHeaders = requestHeaders.Concat(request.Content.Headers);
}
var headers = new List<Header>();
foreach (var item in requestHeaders)
{
foreach (var headerValue in item.Value)
{
headers.Add(new Header { Name = item.Key, Value = headerValue });
}
}
return headers;
}
private static string GetDefaultCredentialsString()
{
// See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials for
// standard values and meanings
switch (DefaultCredentials)
{
case FetchCredentialsOption.Omit:
return "omit";
case FetchCredentialsOption.SameOrigin:
return "same-origin";
case FetchCredentialsOption.Include:
return "include";
default:
throw new ArgumentException($"Unknown credentials option '{DefaultCredentials}'.");
}
}
// Keep these in sync with TypeScript class in Http.ts
private class FetchOptions
{
public string RequestUri { get; set; }
public RequestInit RequestInit { get; set; }
public object RequestInitOverrides { get; set; }
}
private class RequestInit
{
public string Credentials { get; set; }
public IReadOnlyList<Header> Headers { get; set; }
public string Method { get; set; }
}
private class ResponseDescriptor
{
#pragma warning disable 0649
public int StatusCode { get; set; }
public string StatusText { get; set; }
public IReadOnlyList<Header> Headers { get; set; }
#pragma warning restore 0649
public HttpResponseMessage ToResponseMessage(HttpContent content)
{
var result = new HttpResponseMessage((HttpStatusCode)StatusCode);
result.ReasonPhrase = StatusText;
result.Content = content;
var headers = result.Headers;
var contentHeaders = result.Content?.Headers;
foreach (var pair in Headers)
{
if (!headers.TryAddWithoutValidation(pair.Name, pair.Value))
{
contentHeaders?.TryAddWithoutValidation(pair.Name, pair.Value);
}
}
return result;
}
}
private class Header
{
public string Name { get; set; }
public string Value { get; set; }
}
}
}

View File

@ -0,0 +1,59 @@
// 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.Reflection;
namespace Microsoft.AspNetCore.Blazor.Http
{
/// <summary>
/// Configures options for the WebAssembly HTTP message handler.
/// </summary>
public static class WebAssemblyHttpMessageHandlerOptions
{
/// <summary>
/// Gets or sets the default value of the 'credentials' option on outbound HTTP requests.
/// Defaults to <see cref="FetchCredentialsOption.SameOrigin"/>.
/// </summary>
public static FetchCredentialsOption DefaultCredentials
{
get
{
var valueString = MonoDefaultCredentialsGetter.Value();
var result = default(FetchCredentialsOption);
if (valueString != null)
{
Enum.TryParse(valueString, out result);
}
return result;
}
set
{
MonoDefaultCredentialsSetter.Value(value.ToString());
}
}
static Func<Type> MonoWasmHttpMessageHandlerType = ()
=> Assembly.Load("WebAssembly.Net.Http")
.GetType("WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler");
static Func<Type> MonoFetchCredentialsOptionType = ()
=> Assembly.Load("WebAssembly.Net.Http")
.GetType("WebAssembly.Net.Http.HttpClient.FetchCredentialsOption");
static Lazy<PropertyInfo> MonoDefaultCredentialsProperty = new Lazy<PropertyInfo>(
() => MonoWasmHttpMessageHandlerType()?.GetProperty("DefaultCredentials", BindingFlags.Public | BindingFlags.Static));
static Lazy<Func<string>> MonoDefaultCredentialsGetter = new Lazy<Func<string>>(() =>
{
return () => MonoDefaultCredentialsProperty.Value?.GetValue(null).ToString();
});
static Lazy<Action<string>> MonoDefaultCredentialsSetter = new Lazy<Action<string>>(() =>
{
var fetchCredentialsOptionsType = MonoFetchCredentialsOptionType();
return value => MonoDefaultCredentialsProperty.Value?.SetValue(null, Enum.Parse(fetchCredentialsOptionsType, value));
});
}
}

View File

@ -422,7 +422,7 @@
4) Add the file we just created to the list of file writes, to support incremental builds.
-->
<ItemGroup>
<_MonoBaseClassLibraryFolder Include="$(MonoBaseClassLibraryPath);$(MonoBaseClassLibraryFacadesPath)" />
<_MonoBaseClassLibraryFolder Include="$(MonoBaseClassLibraryPath);$(MonoBaseClassLibraryFacadesPath);$(MonoWasmFrameworkPath)" />
<_BlazorAssembliesToLink Include="@(_BlazorDependencyInput->'-a &quot;%(Identity)&quot;')" />
<_BlazorAssembliesToLink Include="@(IntermediateAssembly->'-a &quot;%(FullPath)&quot;')" />
<_BlazorFolderLookupPaths Include="@(_MonoBaseClassLibraryFolder->'-d &quot;%(Identity)&quot;')" />
@ -528,7 +528,7 @@
<PropertyGroup>
<_ReferencesArg Condition="'@(_BlazorDependencyInput)' != ''">--references &quot;$(BlazorResolveDependenciesFilePath)&quot;</_ReferencesArg>
<_BclParameter>--base-class-library &quot;$(MonoBaseClassLibraryPath)&quot; --base-class-library &quot;$(MonoBaseClassLibraryFacadesPath)&quot;</_BclParameter>
<_BclParameter>--base-class-library &quot;$(MonoBaseClassLibraryPath)&quot; --base-class-library &quot;$(MonoBaseClassLibraryFacadesPath)&quot; --base-class-library &quot;$(MonoWasmFrameworkPath)&quot;</_BclParameter>
</PropertyGroup>
<WriteLinesToFile

View File

@ -18,4 +18,10 @@
<type fullname="System.ComponentModel.GuidConverter" />
<type fullname="System.ComponentModel.TimeSpanConverter" />
</assembly>
<assembly fullname="WebAssembly.Net.Http">
<!-- Without this, the setter for DefaultCredentials would be removed, but we need it -->
<type fullname="WebAssembly.Net.Http.HttpClient.FetchCredentialsOption" />
<type fullname="WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler" />
</assembly>
</linker>

View File

@ -40,6 +40,7 @@
<ItemGroup>
<_BclDirectory Include="$(MonoBaseClassLibraryPath)" />
<_BclDirectory Include="$(MonoBaseClassLibraryFacadesPath)" />
<_BclDirectory Include="$(MonoWasmFrameworkPath)" />
</ItemGroup>
<WriteLinesToFile Lines="@(ReferencePath)" File="$(TargetDir)referenceHints.txt" WriteOnlyWhenDifferent="true" Overwrite="true" />

View File

@ -488,7 +488,7 @@ namespace WsProxy {
// Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously
// results in a "Memory access out of bounds", causing 'values' to be null,
// so skip returning variable values in that case.
while (values != null && i < var_ids.Length && i < values.Length) {
while (values != null && i < vars.Length && i < values.Length) {
var value = values [i] ["value"];
if (((string)value ["description"]) == null)
value ["description"] = value ["value"]?.ToString();

View File

@ -20,7 +20,9 @@
'mscorlib.dll',
'System.dll',
'System.Core.dll',
'System.Net.Http.dll'
'System.Net.Http.dll',
'WebAssembly.Bindings.dll',
'WebAssembly.Net.Http.dll'
]);
// For these tests we're using Mono's built-in mono_load_runtime_and_bcl util.

View File

@ -11,14 +11,6 @@ namespace MonoSanityClient
{
public static class Examples
{
static Examples()
{
// We have to populate GetHttpMessageHandler with something (like the real
// Blazor web assembly host does), otherwise HttpClientHandler's constructor
// gets into an infinite loop.
FakeHttpMessageHandler.Attach();
}
public static string AddNumbers(int a, int b)
=> (a + b).ToString();

View File

@ -1,26 +0,0 @@
// 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.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace MonoSanityClient
{
class FakeHttpMessageHandler : HttpMessageHandler
{
public static void Attach()
{
var getHttpMessageHandlerField = typeof(HttpClient).GetField(
"GetHttpMessageHandler",
BindingFlags.Static | BindingFlags.NonPublic);
Func<HttpMessageHandler> handlerFactory = () => new FakeHttpMessageHandler();
getHttpMessageHandlerField.SetValue(null, handlerFactory);
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
=> throw new NotImplementedException($"{nameof(FakeHttpMessageHandler)} cannot {nameof(SendAsync)}.");
}
}

View File

@ -581,6 +581,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
public void OpenRegion(int sequence)
{
// We are entering a new scope, since we track the "duplicate attributes" per
// element/component we might need to clean them up now.
if (_hasSeenAddMultipleAttributes)
{
var indexOfLastElementOrComponent = _openElementIndices.Peek();
ProcessDuplicateAttributes(first: indexOfLastElementOrComponent + 1);
}
_openElementIndices.Push(_entries.Count);
Append(RenderTreeFrame.Region(sequence));
}

View File

@ -298,6 +298,39 @@ namespace Microsoft.AspNetCore.Components.Rendering
frame => AssertFrame.Attribute(frame, "attribute7", "the end"));
}
[Fact]
public void CanAddMultipleAttributes_WithChildRegion()
{
// This represents bug https://github.com/aspnet/AspNetCore/issues/16570
// If a sequence of attributes is terminated by a call to builder.OpenRegion,
// then the attribute deduplication logic wasn't working correctly
// Arrange
var builder = new RenderTreeBuilder();
// Act
builder.OpenElement(0, "myelement");
builder.AddAttribute(0, "attribute1", "value1");
builder.AddMultipleAttributes(1, new Dictionary<string, object>()
{
{ "attribute1", "value2" },
});
builder.OpenRegion(2);
builder.OpenElement(3, "child");
builder.CloseElement();
builder.CloseRegion();
builder.CloseElement();
// Assert
var frames = builder.GetFrames().AsEnumerable().ToArray();
Assert.Collection(
frames,
frame => AssertFrame.Element(frame, "myelement", 4),
frame => AssertFrame.Attribute(frame, "attribute1", "value2"),
frame => AssertFrame.Region(frame, 2, 2),
frame => AssertFrame.Element(frame, "child", 1, 3));
}
[Fact]
public void CanAddMultipleAttributes_DictionaryObject()
{

View File

@ -122,6 +122,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// Report errors asynchronously. InitializeAsync is designed not to throw.
Log.InitializationFailed(_logger, ex);
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex), ex);
}
});
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
@ -153,7 +154,9 @@ namespace Microsoft.AspNetCore.Components.Server
string unprotected;
try
{
unprotected = _dataProtector.Unprotect(record.Descriptor);
var payload = Convert.FromBase64String(record.Descriptor);
var unprotectedBytes = _dataProtector.Unprotect(payload);
unprotected = Encoding.UTF8.GetString(unprotectedBytes);
}
catch (Exception e)
{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ import { getAssemblyNameFromUrl } from './Platform/Url';
import { renderBatch } from './Rendering/Renderer';
import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
import { Pointer } from './Platform/Platform';
import { fetchBootConfigAsync, loadEmbeddedResourcesAsync, shouldAutoStart } from './BootCommon';
import { shouldAutoStart } from './BootCommon';
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
let started = false;
@ -64,6 +64,46 @@ async function boot(options?: any): Promise<void> {
platform.callEntryPoint(mainAssemblyName, bootConfig.entryPoint, []);
}
async function fetchBootConfigAsync() {
// Later we might make the location of this configurable (e.g., as an attribute on the <script>
// element that's importing this file), but currently there isn't a use case for that.
const bootConfigResponse = await fetch('_framework/blazor.boot.json', { method: 'Get', credentials: 'include' });
return bootConfigResponse.json() as Promise<BootJsonData>;
}
function loadEmbeddedResourcesAsync(bootConfig: BootJsonData): Promise<any> {
const cssLoadingPromises = bootConfig.cssReferences.map(cssReference => {
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.href = cssReference;
return loadResourceFromElement(linkElement);
});
const jsLoadingPromises = bootConfig.jsReferences.map(jsReference => {
const scriptElement = document.createElement('script');
scriptElement.src = jsReference;
return loadResourceFromElement(scriptElement);
});
return Promise.all(cssLoadingPromises.concat(jsLoadingPromises));
}
function loadResourceFromElement(element: HTMLElement) {
return new Promise((resolve, reject) => {
element.onload = resolve;
element.onerror = reject;
document.head!.appendChild(element);
});
}
// Keep in sync with BootJsonData in Microsoft.AspNetCore.Blazor.Build
interface BootJsonData {
main: string;
entryPoint: string;
assemblyReferences: string[];
cssReferences: string[];
jsReferences: string[];
linkerEnabled: boolean;
}
window['Blazor'].start = boot;
if (shouldAutoStart()) {
boot();

View File

@ -1,43 +1,3 @@
export async function fetchBootConfigAsync() {
// Later we might make the location of this configurable (e.g., as an attribute on the <script>
// element that's importing this file), but currently there isn't a use case for that.
const bootConfigResponse = await fetch('_framework/blazor.boot.json', { method: 'Get', credentials: 'include' });
return bootConfigResponse.json() as Promise<BootJsonData>;
}
export function loadEmbeddedResourcesAsync(bootConfig: BootJsonData): Promise<any> {
const cssLoadingPromises = bootConfig.cssReferences.map(cssReference => {
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.href = cssReference;
return loadResourceFromElement(linkElement);
});
const jsLoadingPromises = bootConfig.jsReferences.map(jsReference => {
const scriptElement = document.createElement('script');
scriptElement.src = jsReference;
return loadResourceFromElement(scriptElement);
});
return Promise.all(cssLoadingPromises.concat(jsLoadingPromises));
}
function loadResourceFromElement(element: HTMLElement) {
return new Promise((resolve, reject) => {
element.onload = resolve;
element.onerror = reject;
document.head!.appendChild(element);
});
}
// Keep in sync with BootJsonData in Microsoft.AspNetCore.Blazor.Build
interface BootJsonData {
main: string;
entryPoint: string;
assemblyReferences: string[];
cssReferences: string[];
jsReferences: string[];
linkerEnabled: boolean;
}
// Tells you if the script was added without <script src="..." autostart="false"></script>
export function shouldAutoStart() {
return !!(document &&

View File

@ -1,5 +1,4 @@
import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
import { internalFunctions as httpInternalFunctions } from './Services/Http';
import { attachRootComponentToElement } from './Rendering/Renderer';
// Make the following APIs available in global scope for invocation from JS
@ -8,7 +7,6 @@ window['Blazor'] = {
_internal: {
attachRootComponentToElement,
http: httpInternalFunctions,
navigationManager: navigationManagerInternalFunctions,
},
};

View File

@ -21,6 +21,8 @@ const nonBubblingEvents = toLookup([
'DOMNodeRemovedFromDocument',
]);
const disableableEventNames = toLookup(['click', 'dblclick', 'mousedown', 'mousemove', 'mouseup']);
export interface OnEventCallback {
(event: Event, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>, eventFieldInfo: EventFieldInfo | null): void;
}
@ -107,7 +109,7 @@ export class EventDelegator {
const handlerInfos = this.getEventHandlerInfosForElement(candidateElement, false);
if (handlerInfos) {
const handlerInfo = handlerInfos.getHandler(evt.type);
if (handlerInfo) {
if (handlerInfo && !eventIsDisabledOnElement(candidateElement, evt.type)) {
// We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgs) {
eventArgs = EventForDotNet.fromDOMEvent(evt);
@ -269,3 +271,11 @@ function toLookup(items: string[]): { [key: string]: boolean } {
});
return result;
}
function eventIsDisabledOnElement(element: Element, eventName: string): boolean {
// We want to replicate the normal DOM event behavior that, for 'interactive' elements
// with a 'disabled' attribute, certain mouse events are suppressed
return (element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)
&& disableableEventNames.hasOwnProperty(eventName)
&& element.disabled;
}

View File

@ -1,132 +0,0 @@
import { platform } from '../Environment';
import { MethodHandle, System_String, System_Array } from '../Platform/Platform';
const httpClientAssembly = 'Microsoft.AspNetCore.Blazor';
const httpClientNamespace = `${httpClientAssembly}.Http`;
const httpClientTypeName = 'WebAssemblyHttpMessageHandler';
let receiveResponseMethod: MethodHandle;
let allocateArrayMethod: MethodHandle;
// These are the functions we're making available for invocation from .NET
export const internalFunctions = {
sendAsync,
};
async function sendAsync(id: number, body: System_Array<any>, jsonFetchArgs: System_String) {
let response: Response;
let responseData: ArrayBuffer;
const fetchOptions: FetchOptions = JSON.parse(platform.toJavaScriptString(jsonFetchArgs));
const requestInit: RequestInit = Object.assign(convertToRequestInit(fetchOptions.requestInit), fetchOptions.requestInitOverrides);
if (body) {
requestInit.body = platform.toUint8Array(body);
}
try {
response = await fetch(fetchOptions.requestUri, requestInit);
responseData = await response.arrayBuffer();
} catch (ex) {
dispatchErrorResponse(id, ex.toString());
return;
}
dispatchSuccessResponse(id, response, responseData);
}
function convertToRequestInit(blazorRequestInit: BlazorRequestInit) {
return {
credentials: blazorRequestInit.credentials,
method: blazorRequestInit.method,
headers: blazorRequestInit.headers.map(item => [item.name, item.value])
};
}
function dispatchSuccessResponse(id: number, response: Response, responseData: ArrayBuffer) {
const responseDescriptor: ResponseDescriptor = {
statusCode: response.status,
statusText: response.statusText,
headers: [],
};
response.headers.forEach((value, name) => {
responseDescriptor.headers.push({ name: name, value: value });
});
if (!allocateArrayMethod) {
allocateArrayMethod = platform.findMethod(
httpClientAssembly,
httpClientNamespace,
httpClientTypeName,
'AllocateArray'
);
}
// allocate a managed byte[] of the right size
const dotNetArray = platform.callMethod(allocateArrayMethod, null, [platform.toDotNetString(responseData.byteLength.toString())]) as System_Array<any>;
// get an Uint8Array view of it
const array = platform.toUint8Array(dotNetArray);
// copy the responseData to our managed byte[]
array.set(new Uint8Array(responseData));
dispatchResponse(
id,
platform.toDotNetString(JSON.stringify(responseDescriptor)),
dotNetArray,
/* errorMessage */ null
);
}
function dispatchErrorResponse(id: number, errorMessage: string) {
dispatchResponse(
id,
/* responseDescriptor */ null,
/* responseText */ null,
platform.toDotNetString(errorMessage)
);
}
function dispatchResponse(id: number, responseDescriptor: System_String | null, responseData: System_Array<any> | null, errorMessage: System_String | null) {
if (!receiveResponseMethod) {
receiveResponseMethod = platform.findMethod(
httpClientAssembly,
httpClientNamespace,
httpClientTypeName,
'ReceiveResponse'
);
}
platform.callMethod(receiveResponseMethod, null, [
platform.toDotNetString(id.toString()),
responseDescriptor,
responseData,
errorMessage,
]);
}
// Keep these in sync with the .NET equivalent in WebAssemblyHttpMessageHandler.cs
interface FetchOptions {
requestUri: string;
requestInit: BlazorRequestInit;
requestInitOverrides: RequestInit;
}
interface BlazorRequestInit {
credentials: string;
headers: Header[];
method: string;
}
interface ResponseDescriptor {
// We don't have BodyText in here because if we did, then in the JSON-response case (which
// is the most common case), we'd be double-encoding it, since the entire ResponseDescriptor
// also gets JSON encoded. It would work but is twice the amount of string processing.
statusCode: number;
statusText: string;
headers: Header[];
}
interface Header {
name: string;
value: string;
}

View File

@ -207,6 +207,37 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("abcdefghijklmn", () => output.Text);
}
[Fact]
public void NonInteractiveElementWithDisabledAttributeDoesRespondToMouseEvents()
{
Browser.MountTestComponent<EventDisablingComponent>();
var element = Browser.FindElement(By.Id("disabled-div"));
var eventLog = Browser.FindElement(By.Id("event-log"));
Browser.Equal(string.Empty, () => eventLog.GetAttribute("value"));
element.Click();
Browser.Equal("Got event on div", () => eventLog.GetAttribute("value"));
}
[Theory]
[InlineData("#disabled-button")]
[InlineData("#disabled-button span")]
[InlineData("#disabled-textarea")]
public void InteractiveElementWithDisabledAttributeDoesNotRespondToMouseEvents(string elementSelector)
{
Browser.MountTestComponent<EventDisablingComponent>();
var element = Browser.FindElement(By.CssSelector(elementSelector));
var eventLog = Browser.FindElement(By.Id("event-log"));
Browser.Equal(string.Empty, () => eventLog.GetAttribute("value"));
element.Click();
// It's no use observing that the log is still empty, since maybe the UI just hasn't updated yet
// To be sure that the preceding action has no effect, we need to trigger a different action that does have an effect
Browser.FindElement(By.Id("enabled-button")).Click();
Browser.Equal("Got event on enabled button", () => eventLog.GetAttribute("value"));
}
void SendKeysSequentially(IWebElement target, string text)
{
// Calling it for each character works around some chars being skipped

View File

@ -89,11 +89,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact]
public void CanSendRequestHeaders()
{
AddRequestHeader("TestHeader", "Value from test");
AddRequestHeader("testheader", "Value from test");
AddRequestHeader("another-header", "Another value");
IssueRequest("DELETE", "/subdir/api/person");
Assert.Equal("OK", _responseStatus.Text);
Assert.Contains("TestHeader: Value from test", _responseBody.Text);
Assert.Contains("testheader: Value from test", _responseBody.Text);
Assert.Contains("another-header: Another value", _responseBody.Text);
}
@ -107,15 +107,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("{\"id\":123,\"name\":\"Bert\"}", _responseBody.Text);
}
[Fact]
public void CanSetRequestReferer()
{
SetValue("request-referrer", "/test-referrer");
IssueRequest("GET", "/subdir/api/person/referrer");
Assert.Equal("OK", _responseStatus.Text);
Assert.EndsWith("/test-referrer", _responseBody.Text);
}
[Fact]
public void CanSendAndReceiveCookies()
{

View File

@ -0,0 +1,36 @@
<h3 id="event-disabling">Disabling events</h3>
<p>
<div @onclick="@(() => LogEvent("Got event on div"))" id="disabled-div" disabled style="border: 1px dashed red">Even though this div has a 'disabled' attribute, it still responds to mouse events, because you can't disable a div</div>
</p>
<p>
<button @onclick="@(() => LogEvent("Got event on disabled button"))" id="disabled-button" disabled>Since this button is disabled, it doesn't react to mouse events <span style="border: 1px dashed green">even on its children</span></button>
</p>
<p>
<textarea @onclick="@(() => LogEvent("Got event on disabled textarea"))" id="disabled-textarea" disabled>Since this textarea is disabled, it doesn't react to mouse events</textarea>
</p>
<p>
<button @onclick="@(() => LogEvent("Got event on enabled button"))" id="enabled-button">Since this button is enabled, it does react to mouse events <span style="border: 1px dashed green">and so do its children</span></button>
</p>
<h3>Event log</h3>
<textarea id="event-log" readonly @bind="logValue"></textarea>
<button id="clear-log" @onclick="@(() => { logValue = string.Empty; })">Clear log</button>
@code {
string logValue = string.Empty;
void LogEvent(string message)
{
if (logValue != string.Empty)
{
logValue += Environment.NewLine;
}
logValue += message;
}
}

View File

@ -1,6 +1,5 @@
@using System.Net
@using System.Net.Http
@using Microsoft.AspNetCore.Blazor.Http
@inject HttpClient Http
<h1>HTTP request tester</h1>
@ -38,11 +37,6 @@
<button id="add-header" @onclick="AddHeader">Add</button>
</p>
<p>
<div>Request referrer:</div>
<input id="request-referrer" type="text" @bind=requestReferrer />
</p>
<button id="send-request" @onclick="DoRequest">Request</button>
@if (responseStatusCode.HasValue)
@ -65,7 +59,6 @@
string method = "GET";
string requestBody = "";
List<RequestHeader> requestHeaders = new List<RequestHeader>();
string requestReferrer = "";
HttpStatusCode? responseStatusCode;
string responseBody;
@ -103,14 +96,6 @@
}
}
if (!string.IsNullOrEmpty(requestReferrer))
{
requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
{
referrer = requestReferrer
};
}
var response = await Http.SendAsync(requestMessage);
responseStatusCode = response.StatusCode;
responseBody = await response.Content.ReadAsStringAsync();

View File

@ -24,6 +24,7 @@
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
<option value="BasicTestApp.EventDisablingComponent">Event disabling</option>
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
<option value="BasicTestApp.FocusEventComponent">Focus events</option>

View File

@ -30,7 +30,7 @@ namespace BasicTestApp
{
// Needed because the test server runs on a different port than the client app,
// and we want to test sending/receiving cookies underling this config
WebAssemblyHttpMessageHandler.DefaultCredentials = FetchCredentialsOption.Include;
WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
}
app.AddComponent<Index>("root");

View File

@ -2,6 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
@ -29,7 +33,7 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
public StaticWebAssetsFileProvider(string pathPrefix, string contentRoot)
{
BasePath = new PathString(pathPrefix.StartsWith("/") ? pathPrefix : "/" + pathPrefix);
BasePath = NormalizePath(pathPrefix);
InnerProvider = new PhysicalFileProvider(contentRoot);
}
@ -40,20 +44,30 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
/// <inheritdoc />
public IDirectoryContents GetDirectoryContents(string subpath)
{
if (!StartsWithBasePath(subpath, out var physicalPath))
{
return NotFoundDirectoryContents.Singleton;
}
else
var modifiedSub = NormalizePath(subpath);
if (StartsWithBasePath(modifiedSub, out var physicalPath))
{
return InnerProvider.GetDirectoryContents(physicalPath.Value);
}
else if (string.Equals(subpath, string.Empty) || string.Equals(modifiedSub, "/"))
{
return new StaticWebAssetsDirectoryRoot(BasePath);
}
else if (BasePath.StartsWithSegments(modifiedSub, FilePathComparison, out var remaining))
{
return new StaticWebAssetsDirectoryRoot(remaining);
}
return NotFoundDirectoryContents.Singleton;
}
/// <inheritdoc />
public IFileInfo GetFileInfo(string subpath)
{
if (!StartsWithBasePath(subpath, out var physicalPath))
var modifiedSub = NormalizePath(subpath);
if (!StartsWithBasePath(modifiedSub, out var physicalPath))
{
return new NotFoundFileInfo(subpath);
}
@ -69,9 +83,69 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
return InnerProvider.Watch(filter);
}
private static string NormalizePath(string path)
{
path = path.Replace('\\', '/');
return path != null && path.StartsWith("/") ? path : "/" + path;
}
private bool StartsWithBasePath(string subpath, out PathString rest)
{
return new PathString(subpath).StartsWithSegments(BasePath, FilePathComparison, out rest);
}
private class StaticWebAssetsDirectoryRoot : IDirectoryContents
{
private readonly string _nextSegment;
public StaticWebAssetsDirectoryRoot(PathString remainingPath)
{
// We MUST use the Value property here because it is unescaped.
_nextSegment = remainingPath.Value.Split("/", StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
}
public bool Exists => true;
public IEnumerator<IFileInfo> GetEnumerator()
{
return GenerateEnum();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GenerateEnum();
}
private IEnumerator<IFileInfo> GenerateEnum()
{
return new[] { new StaticWebAssetsFileInfo(_nextSegment) }
.Cast<IFileInfo>().GetEnumerator();
}
private class StaticWebAssetsFileInfo : IFileInfo
{
public StaticWebAssetsFileInfo(string name)
{
Name = name;
}
public bool Exists => true;
public long Length => throw new NotImplementedException();
public string PhysicalPath => throw new NotImplementedException();
public DateTimeOffset LastModified => throw new NotImplementedException();
public bool IsDirectory => true;
public string Name { get; }
public Stream CreateReadStream()
{
throw new NotImplementedException();
}
}
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@ -9,7 +9,6 @@
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestEventListener.cs" />
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestCounterListener.cs" />
<Content Include="testroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<None Remove="testroot\wwwroot\Static Web Assets.txt" />
<Content Include="Microsoft.AspNetCore.Hosting.StaticWebAssets.xml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -37,6 +37,75 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
Assert.Equal("/_content", provider.BasePath);
}
[Theory]
[InlineData("\\", "_content")]
[InlineData("\\_content\\RazorClassLib\\Dir", "Castle.Core.dll")]
[InlineData("", "_content")]
[InlineData("/", "_content")]
[InlineData("/_content", "RazorClassLib")]
[InlineData("/_content/RazorClassLib", "Dir")]
[InlineData("/_content/RazorClassLib/Dir", "Microsoft.AspNetCore.Hosting.Tests.dll")]
[InlineData("/_content/RazorClassLib/Dir/testroot/", "TextFile.txt")]
[InlineData("/_content/RazorClassLib/Dir/testroot/wwwroot/", "README")]
public void GetDirectoryContents_WalksUpContentRoot(string searchDir, string expected)
{
// Arrange
var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib/Dir", AppContext.BaseDirectory);
// Act
var directory = provider.GetDirectoryContents(searchDir);
// Assert
Assert.NotEmpty(directory);
Assert.Contains(directory, file => string.Equals(file.Name, expected));
}
[Fact]
public void GetDirectoryContents_DoesNotFindNonExistentFiles()
{
// Arrange
var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib/", AppContext.BaseDirectory);
// Act
var directory = provider.GetDirectoryContents("/_content/RazorClassLib/False");
// Assert
Assert.Empty(directory);
}
[Theory]
[InlineData("/False/_content/RazorClassLib/")]
[InlineData("/_content/RazorClass")]
public void GetDirectoryContents_PartialMatchFails(string requestedUrl)
{
// Arrange
var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib", AppContext.BaseDirectory);
// Act
var directory = provider.GetDirectoryContents(requestedUrl);
// Assert
Assert.Empty(directory);
}
[Fact]
public void GetDirectoryContents_HandlesWhitespaceInBase()
{
// Arrange
var provider = new StaticWebAssetsFileProvider("/_content/Static Web Assets",
Path.Combine(AppContext.BaseDirectory, "testroot", "wwwroot"));
// Act
var directory = provider.GetDirectoryContents("/_content/Static Web Assets/Static Web/");
// Assert
Assert.Collection(directory,
file =>
{
Assert.Equal("Static Web.txt", file.Name);
});
}
[Fact]
public void StaticWebAssetsFileProvider_FindsFileWithSpaces()
{

View File

@ -489,6 +489,55 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never());
}
[Fact]
public async Task ReadAsync_WithEnableBufferingWorks()
{
// Arrange
var formatter = GetInputFormatter();
var content = "{\"name\": \"Test\"}";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
httpContext.Request.EnableBuffering();
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
var userModel = Assert.IsType<ComplexModel>(result.Model);
Assert.Equal("Test", userModel.Name);
var requestBody = httpContext.Request.Body;
requestBody.Position = 0;
Assert.Equal(content, new StreamReader(requestBody).ReadToEnd());
}
[Fact]
public async Task ReadAsync_WithEnableBufferingWorks_WithInputStreamAtOffset()
{
// Arrange
var formatter = GetInputFormatter();
var content = "abc{\"name\": \"Test\"}";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
httpContext.Request.EnableBuffering();
var requestBody = httpContext.Request.Body;
requestBody.Position = 3;
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
var userModel = Assert.IsType<ComplexModel>(result.Model);
Assert.Equal("Test", userModel.Name);
requestBody.Position = 0;
Assert.Equal(content, new StreamReader(requestBody).ReadToEnd());
}
internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; }
internal abstract string JsonFormatter_EscapedKeys_Expected { get; }

View File

@ -120,7 +120,17 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Stream readStream = new NonDisposableStream(request.Body);
var disposeReadStream = false;
if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering)
if (readStream.CanSeek)
{
// The most common way of getting here is the user has request buffering on.
// However, request buffering isn't eager, and consequently it will peform pass-thru synchronous
// reads as part of the deserialization.
// To avoid this, drain and reset the stream.
var position = request.Body.Position;
await readStream.DrainAsync(CancellationToken.None);
readStream.Position = position;
}
else if (!_options.SuppressInputFormatterBuffering)
{
// XmlDataContractSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously
// read everything into a buffer, and then seek back to the beginning.

View File

@ -101,7 +101,17 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Stream readStream = new NonDisposableStream(request.Body);
var disposeReadStream = false;
if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering)
if (readStream.CanSeek)
{
// The most common way of getting here is the user has request buffering on.
// However, request buffering isn't eager, and consequently it will peform pass-thru synchronous
// reads as part of the deserialization.
// To avoid this, drain and reset the stream.
var position = request.Body.Position;
await readStream.DrainAsync(CancellationToken.None);
readStream.Position = position;
}
else if (!_options.SuppressInputFormatterBuffering)
{
// XmlSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously
// read everything into a buffer, and then seek back to the beginning.

View File

@ -130,7 +130,17 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var readStream = request.Body;
var disposeReadStream = false;
if (!request.Body.CanSeek && !suppressInputFormatterBuffering)
if (readStream.CanSeek)
{
// The most common way of getting here is the user has request buffering on.
// However, request buffering isn't eager, and consequently it will peform pass-thru synchronous
// reads as part of the deserialization.
// To avoid this, drain and reset the stream.
var position = request.Body.Position;
await readStream.DrainAsync(CancellationToken.None);
readStream.Position = position;
}
else if (!suppressInputFormatterBuffering)
{
// JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously
// read everything into a buffer, and then seek back to the beginning.

View File

@ -65,6 +65,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
for (var i = 0; i < candidates.Count; i++)
{
if (!candidates.IsValidCandidate(i))
{
continue;
}
ref var candidate = ref candidates[i];
var endpoint = candidate.Endpoint;

View File

@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("params", DictionaryAttributePrefix="param-")]
public System.Collections.Generic.IDictionary<string, object> Parameters { get { throw null; } set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")]
public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { get { throw null; } set { } }
[Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute]
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute]
public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

View File

@ -14,14 +14,16 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// <summary>
/// A <see cref="TagHelper"/> that renders a Razor component.
/// </summary>
[HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
[HtmlTargetElement(TagHelperName, Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
public sealed class ComponentTagHelper : TagHelper
{
private const string TagHelperName = "component";
private const string ComponentParameterName = "params";
private const string ComponentParameterPrefix = "param-";
private const string ComponentTypeName = "type";
private const string RenderModeName = "render-mode";
private IDictionary<string, object> _parameters;
private RenderMode? _renderMode;
/// <summary>
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
@ -54,7 +56,29 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// Gets or sets the <see cref="Rendering.RenderMode"/>
/// </summary>
[HtmlAttributeName(RenderModeName)]
public RenderMode RenderMode { get; set; }
public RenderMode RenderMode
{
get => _renderMode ?? default;
set
{
switch (value)
{
case RenderMode.Server:
case RenderMode.ServerPrerendered:
case RenderMode.Static:
_renderMode = value;
break;
default:
throw new ArgumentException(
message: Resources.FormatInvalidEnumArgument(
nameof(value),
value,
typeof(RenderMode).FullName),
paramName: nameof(value));
}
}
}
/// <inheritdoc />
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
@ -69,6 +93,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(output));
}
if (_renderMode is null)
{
throw new ArgumentException(Resources.FormatAttributeIsRequired(RenderModeName, TagHelperName), nameof(RenderMode));
}
var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters);

View File

@ -162,4 +162,7 @@
<data name="ViewEngine_FallbackViewNotFound" xml:space="preserve">
<value>The fallback partial view '{0}' was not found. The following locations were searched:{1}</value>
</data>
<data name="AttributeIsRequired" xml:space="preserve">
<value>A value for the '{0}' attribute must be supplied to the '{1}' tag helper.</value>
</data>
</root>

View File

@ -3,13 +3,13 @@
using System;
using System.Collections.Generic;
using System.IO.Pipes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var tagHelper = new ComponentTagHelper
{
ViewContext = GetViewContext(),
RenderMode = RenderMode.Static,
};
var context = GetTagHelperContext();
var output = GetTagHelperOutput();
@ -38,6 +39,24 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Null(output.TagName);
}
[Fact]
public async Task ProcessAsync_WithoutSpecifyingRenderMode_ThrowsError()
{
// Arrange
var tagHelper = new ComponentTagHelper
{
ViewContext = GetViewContext(),
};
var context = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act & Assert
await ExceptionAssert.ThrowsArgumentAsync(
() => tagHelper.ProcessAsync(context, output),
nameof(RenderMode),
"A value for the 'render-mode' attribute must be supplied to the 'component' tag helper.");
}
private static TagHelperContext GetTagHelperContext()
{
return new TagHelperContext(

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.DataProtection;
@ -42,8 +43,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
values,
invocationId.Value);
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
return (serverComponent.Sequence, _dataProtector.Protect(serializedServerComponent, ServerComponentSerializationSettings.DataExpiration));
var serializedServerComponentBytes = JsonSerializer.SerializeToUtf8Bytes(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
var protectedBytes = _dataProtector.Protect(serializedServerComponentBytes, ServerComponentSerializationSettings.DataExpiration);
return (serverComponent.Sequence, Convert.ToBase64String(protectedBytes));
}
internal IEnumerable<string> GetPreamble(ServerComponentMarker record)

View File

@ -16,6 +16,7 @@
<ItemGroup>
<Compile Include="..\..\Mvc.Formatters.Xml\test\XmlAssert.cs" />
<EmbeddedResource Include="compiler\resources\**\*" />
<None Remove="compiler\resources\TagHelpersWebSite.Home.GlobbingTagHelpers.html" />
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -6,6 +6,8 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using RoutingWebSite;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@ -14,12 +16,13 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public RoutingDynamicTest(MvcTestFixture<RoutingWebSite.StartupForDynamic> fixture)
{
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
Client = factory.CreateDefaultClient();
Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
Client = Factory.CreateDefaultClient();
}
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup<RoutingWebSite.StartupForDynamic>();
public WebApplicationFactory<StartupForDynamic> Factory { get; }
public HttpClient Client { get; }
[Fact]
@ -99,5 +102,39 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello from dynamic page: /DynamicPage", content);
}
[Fact]
public async Task AppWithDynamicRouteAndMapRazorPages_CanRouteToRazorPage()
{
// Regression test for https://github.com/aspnet/AspNetCore/issues/13996
// Arrange
var client = Factory.WithWebHostBuilder(b => b.UseStartup<StartupForDynamicAndRazorPages>()).CreateDefaultClient();
var url = "/PageWithLinks";
// Act
var response = await client.GetAsync(url);
// Assert
var document = await response.GetHtmlDocumentAsync();
var editLink = document.RequiredQuerySelector("#editlink");
Assert.Equal("/Edit/10", editLink.GetAttribute("href"));
}
[Fact]
public async Task AppWithDynamicRouteAndMapRazorPages_CanRouteToDynamicController()
{
// Regression test for https://github.com/aspnet/AspNetCore/issues/13996
// Arrange
var client = Factory.WithWebHostBuilder(b => b.UseStartup<StartupForDynamicAndRazorPages>()).CreateDefaultClient();
var url = "/de/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
// Act
var response = await client.GetAsync(url);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
Assert.StartsWith("Hello from dynamic controller", content);
}
}
}

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public HttpClient EncodedClient { get; }
[Theory]
[InlineData("GlobbingTagHelpers")]
[InlineData("Index")]
[InlineData("About")]
[InlineData("Help")]

View File

@ -0,0 +1,39 @@

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Globbing Page - My ASP.NET Application</title>
</head>
<body>
<h1 style="font-family: cursive;">ASP.NET vNext - Globbing Page</h1>
<p>| <a href="/" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Home</a>
| <a href="/home/about" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My About</a>
| <a href="/home/help" style="background-color: gray;color: white;border-radius: 3px;border: 1px solid black;padding: 3px;font-family: cursive;">My Help</a> |</p>
<div>
Globbing
<hr />
<footer>
<!-- File wildcard -->
<script src="/js/dist/dashboardHome.js"></script>
<!-- RazorClassLib folder wildcard -->
<script src="/_content/RazorPagesClassLibrary/razorlib/file.js"></script>
<!-- RazorClassLib deep wildcard -->
<script src="/js/dist/dashboardHome.js"></script><script src="/_content/RazorPagesClassLibrary/razorlib/file.js"></script>
<!-- RazorClassLib Exclude local -->
<script src="/_content/RazorPagesClassLibrary/razorlib/file.js"></script>
<!-- local Exclude lib-->
<script src="/js/dist/dashboardHome.js"></script>
</footer>
</div>
</body>
</html>

View File

@ -5,7 +5,7 @@
<IsTestAssetProject>true</IsTestAssetProject>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
</ItemGroup>

View File

@ -0,0 +1,5 @@
window.exampleJsFunctions = {
showPrompt: function (message) {
return prompt(message, 'Type anything here')
}
};

View File

@ -0,0 +1,62 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
namespace RoutingWebSite
{
// For by tests for a mix of dynamic routing + Razor Pages
public class StartupForDynamicAndRazorPages
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Latest);
services.AddSingleton<Transformer>();
// Used by some controllers defined in this project.
services.Configure<RouteOptions>(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapDynamicControllerRoute<Transformer>("{language}/{**slug}");
});
}
private class Transformer : DynamicRouteValueTransformer
{
// Turns a format like `controller=Home,action=Index` into an RVD
public override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
if (!(values["slug"] is string slug))
{
return new ValueTask<RouteValueDictionary>(values);
}
var kvps = slug.Split(",");
var results = new RouteValueDictionary();
foreach (var kvp in kvps)
{
var split = kvp.Split("=");
results[split[0]] = split[1];
}
return new ValueTask<RouteValueDictionary>(results);
}
}
}
}

View File

@ -25,6 +25,11 @@ namespace TagHelpersWebSite.Controllers
return View();
}
public IActionResult GlobbingTagHelpers()
{
return View();
}
public IActionResult Help()
{
return View();

View File

@ -21,6 +21,7 @@ namespace TagHelpersWebSite
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseStaticFiles();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
@ -38,6 +39,7 @@ namespace TagHelpersWebSite
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
new WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStaticWebAssets()
.UseStartup<Startup>()
.UseKestrel()
.UseIISIntegration();

View File

@ -6,6 +6,10 @@
<IsTestAssetProject>true</IsTestAssetProject>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RazorPagesClassLibrary\RazorPagesClassLibrary.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />

View File

@ -0,0 +1,19 @@
@{
ViewData["Title"] = "Globbing Page";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@section footerContent {
<!-- File wildcard -->
<script asp-src-include="/js/dist/dashboard*.js"></script>
<!-- RazorClassLib folder wildcard -->
<script asp-src-include="/_content/RazorPagesClassLibrary/**/file.js"></script>
<!-- RazorClassLib deep wildcard -->
<script asp-src-include="/**/*.js"></script>
<!-- RazorClassLib Exclude local -->
<script asp-src-exclude="/js/dist/*.js" asp-src-include="**/*.js"></script>
<!-- local Exclude lib-->
<script asp-src-exclude="/_content/**/*.js" asp-src-include="**/*.js"></script>
}
Globbing

View File

@ -0,0 +1 @@


View File

@ -1,6 +1,9 @@
@page "/"
@namespace BlazorServerWeb_CSharp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">

View File

@ -12,47 +12,47 @@
},
"private": true,
"dependencies": {
"@angular/animations": "8.0.0",
"@angular/common": "8.0.0",
"@angular/compiler": "8.0.0",
"@angular/core": "8.0.0",
"@angular/forms": "8.0.0",
"@angular/platform-browser": "8.0.0",
"@angular/platform-browser-dynamic": "8.0.0",
"@angular/platform-server": "8.0.0",
"@angular/router": "8.0.0",
"@nguniversal/module-map-ngfactory-loader": "8.0.0-rc.1",
"@angular/animations": "8.2.12",
"@angular/common": "8.2.12",
"@angular/compiler": "8.2.12",
"@angular/core": "8.2.12",
"@angular/forms": "8.2.12",
"@angular/platform-browser": "8.2.12",
"@angular/platform-browser-dynamic": "8.2.12",
"@angular/platform-server": "8.2.12",
"@angular/router": "8.2.12",
"@nguniversal/module-map-ngfactory-loader": "8.1.1",
"aspnet-prerendering": "^3.0.1",
"bootstrap": "^4.3.1",
"core-js": "^2.6.5",
"core-js": "^3.3.3",
"jquery": "3.4.1",
"oidc-client": "^1.9.0",
"popper.js": "^1.14.3",
"rxjs": "^6.4.0",
"zone.js": "~0.9.1"
"oidc-client": "^1.9.1",
"popper.js": "^1.16.0",
"rxjs": "^6.5.3",
"zone.js": "0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.800.6",
"@angular/cli": "8.0.6",
"@angular/compiler-cli": "8.0.0",
"@angular/language-service": "8.0.0",
"@types/jasmine": "~3.3.9",
"@types/jasminewd2": "~2.0.6",
"@types/node": "~11.10.5",
"codelyzer": "^5.0.1",
"jasmine-core": "~3.3.0",
"@angular-devkit/build-angular": "^0.803.14",
"@angular/cli": "8.3.14",
"@angular/compiler-cli": "8.2.12",
"@angular/language-service": "8.2.12",
"@types/jasmine": "~3.4.4",
"@types/jasminewd2": "~2.0.8",
"@types/node": "~12.11.6",
"codelyzer": "^5.2.0",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "^4.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.5",
"karma": "^4.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.0",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"typescript": "3.4.5"
"karma-jasmine-html-reporter": "^1.4.2",
"typescript": "3.5.3"
},
"optionalDependencies": {
"node-sass": "^4.9.3",
"protractor": "~5.4.0",
"ts-node": "~5.0.1",
"tslint": "~5.9.1"
"node-sass": "^4.12.0",
"protractor": "~5.4.2",
"ts-node": "~8.4.1",
"tslint": "~5.20.0"
}
}

View File

@ -1,3 +1,7 @@
{
"parser": "typescript-eslint-parser"
}
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
}

View File

@ -4,39 +4,40 @@
"private": true,
"dependencies": {
"bootstrap": "^4.3.1",
"connected-react-router": "6.2.1",
"history": "4.7.2",
"connected-react-router": "6.5.2",
"history": "4.10.1",
"jquery": "^3.4.1",
"merge": "1.2.1",
"popper.js": "^1.14.7",
"react": "16.7.0",
"react-dom": "16.7.0",
"react-redux": "6.0.0",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
"react-scripts": "^3.0.1",
"reactstrap": "7.0.2",
"redux": "4.0.1",
"popper.js": "^1.16.0",
"react": "16.11.0",
"react-dom": "16.11.0",
"react-redux": "7.1.1",
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"react-scripts": "^3.2.0",
"reactstrap": "8.1.1",
"redux": "4.0.4",
"redux-thunk": "2.3.0",
"typescript": "3.2.2"
"svgo": "1.3.0"
},
"devDependencies": {
"@types/history": "4.7.2",
"@types/jest": "23.3.11",
"@types/node": "10.12.18",
"@types/react": "16.7.18",
"@types/react-dom": "16.0.11",
"@types/react-redux": "6.0.11",
"@types/react-router": "4.4.3",
"@types/react-router-dom": "4.3.1",
"@types/reactstrap": "6.4.3",
"cross-env": "5.2.0",
"eslint": "^5.16.0",
"eslint-plugin-flowtype": "2.50.3",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jsx-a11y": "6.1.2",
"eslint-plugin-react": "7.11.1",
"typescript-eslint-parser": "21.0.2"
"@types/history": "4.7.3",
"@types/jest": "24.0.19",
"@types/node": "12.11.6",
"@types/react": "16.9.9",
"@types/react-dom": "16.9.2",
"@types/react-redux": "7.1.5",
"@types/react-router": "5.1.2",
"@types/react-router-dom": "5.1.0",
"@types/reactstrap": "8.0.6",
"@typescript-eslint/parser": "^2.5.0",
"cross-env": "6.0.3",
"eslint": "^6.5.1",
"eslint-plugin-flowtype": "^3.13.0",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.16.0",
"typescript": "3.6.4"
},
"scripts": {
"start": "react-scripts start",

View File

@ -104,11 +104,13 @@ namespace Templates.Test.Helpers
public async Task ContainsLinks(Page page)
{
var request = new HttpRequestMessage(
HttpMethod.Get,
new Uri(ListeningUri, page.Url));
var response = await RequestWithRetries(client => client.SendAsync(request), _httpClient);
var response = await RequestWithRetries(client =>
{
var request = new HttpRequestMessage(
HttpMethod.Get,
new Uri(ListeningUri, page.Url));
return client.SendAsync(request);
}, _httpClient);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var parser = new HtmlParser();
@ -235,16 +237,18 @@ namespace Templates.Test.Helpers
public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null)
{
var request = new HttpRequestMessage(
HttpMethod.Get,
new Uri(ListeningUri, requestUrl));
var response = await RequestWithRetries(client => {
var request = new HttpRequestMessage(
HttpMethod.Get,
new Uri(ListeningUri, requestUrl));
if (!string.IsNullOrEmpty(acceptContentType))
{
request.Headers.Add("Accept", acceptContentType);
}
if (!string.IsNullOrEmpty(acceptContentType))
{
request.Headers.Add("Accept", acceptContentType);
}
var response = await RequestWithRetries(client => client.SendAsync(request), _httpClient);
return client.SendAsync(request);
}, _httpClient);
Assert.True(statusCode == response.StatusCode, $"Expected {requestUrl} to have status '{statusCode}' but it was '{response.StatusCode}'.");
}