Merge branch 'release/3.1-preview1' => 'release/3.1' (#14645)
This commit is contained in:
commit
43456141e8
|
|
@ -13,21 +13,21 @@
|
|||
<Uri>https://github.com/aspnet/Blazor</Uri>
|
||||
<Sha>348e050ecd9bd8924581afb677089ae5e2d5e508</Sha>
|
||||
</Dependency>
|
||||
<Dependency Name="Microsoft.AspNetCore.Razor.Language" Version="3.1.0-preview1.19472.1">
|
||||
<Dependency Name="Microsoft.AspNetCore.Razor.Language" Version="3.1.0-preview1.19501.1">
|
||||
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
|
||||
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
|
||||
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
|
||||
</Dependency>
|
||||
<Dependency Name="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="3.1.0-preview1.19472.1">
|
||||
<Dependency Name="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="3.1.0-preview1.19501.1">
|
||||
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
|
||||
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
|
||||
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
|
||||
</Dependency>
|
||||
<Dependency Name="Microsoft.CodeAnalysis.Razor" Version="3.1.0-preview1.19472.1">
|
||||
<Dependency Name="Microsoft.CodeAnalysis.Razor" Version="3.1.0-preview1.19501.1">
|
||||
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
|
||||
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
|
||||
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
|
||||
</Dependency>
|
||||
<Dependency Name="Microsoft.NET.Sdk.Razor" Version="3.1.0-preview1.19472.1">
|
||||
<Dependency Name="Microsoft.NET.Sdk.Razor" Version="3.1.0-preview1.19501.1">
|
||||
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
|
||||
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
|
||||
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
|
||||
</Dependency>
|
||||
<Dependency Name="dotnet-ef" Version="3.1.0-preview1.19472.2">
|
||||
<Uri>https://github.com/aspnet/EntityFrameworkCore</Uri>
|
||||
|
|
|
|||
|
|
@ -163,10 +163,10 @@
|
|||
<MicrosoftEntityFrameworkCoreToolsPackageVersion>3.1.0-preview1.19472.2</MicrosoftEntityFrameworkCoreToolsPackageVersion>
|
||||
<MicrosoftEntityFrameworkCorePackageVersion>3.1.0-preview1.19472.2</MicrosoftEntityFrameworkCorePackageVersion>
|
||||
<!-- Packages from aspnet/AspNetCore-Tooling -->
|
||||
<MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>3.1.0-preview1.19472.1</MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>
|
||||
<MicrosoftAspNetCoreRazorLanguagePackageVersion>3.1.0-preview1.19472.1</MicrosoftAspNetCoreRazorLanguagePackageVersion>
|
||||
<MicrosoftCodeAnalysisRazorPackageVersion>3.1.0-preview1.19472.1</MicrosoftCodeAnalysisRazorPackageVersion>
|
||||
<MicrosoftNETSdkRazorPackageVersion>3.1.0-preview1.19472.1</MicrosoftNETSdkRazorPackageVersion>
|
||||
<MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>3.1.0-preview1.19501.1</MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>
|
||||
<MicrosoftAspNetCoreRazorLanguagePackageVersion>3.1.0-preview1.19501.1</MicrosoftAspNetCoreRazorLanguagePackageVersion>
|
||||
<MicrosoftCodeAnalysisRazorPackageVersion>3.1.0-preview1.19501.1</MicrosoftCodeAnalysisRazorPackageVersion>
|
||||
<MicrosoftNETSdkRazorPackageVersion>3.1.0-preview1.19501.1</MicrosoftNETSdkRazorPackageVersion>
|
||||
</PropertyGroup>
|
||||
<!--
|
||||
|
||||
|
|
|
|||
|
|
@ -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.Collections.Immutable;
|
||||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Analyzers
|
|||
{
|
||||
// ASP
|
||||
BuildServiceProviderShouldNotCalledInConfigureServicesMethod,
|
||||
IncorrectlyConfiguredAuthorizationMiddleware,
|
||||
|
||||
// MVC
|
||||
UnsupportedUseMvcWithEndpointRouting,
|
||||
|
|
@ -42,6 +43,15 @@ namespace Microsoft.AspNetCore.Analyzers
|
|||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
helpLinkUri: "https://aka.ms/YJggeFn");
|
||||
|
||||
internal readonly static DiagnosticDescriptor IncorrectlyConfiguredAuthorizationMiddleware = new DiagnosticDescriptor(
|
||||
"ASP0001",
|
||||
"Authorization middleware is incorrectly configured.",
|
||||
"The call to UseAuthorization should appear between app.UseRouting() and app.UseEndpoints(..) for authorization to be correctly evaluated.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
helpLinkUri: "https://aka.ms/AA64fv1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -82,6 +82,7 @@ namespace Microsoft.AspNetCore.Analyzers
|
|||
var analysis = builder.Build();
|
||||
new UseMvcAnalyzer(analysis).AnalyzeSymbol(context);
|
||||
new BuildServiceProviderValidator(analysis).AnalyzeSymbol(context);
|
||||
new UseAuthorizationAnalyzer(analysis).AnalyzeSymbol(context);
|
||||
});
|
||||
|
||||
}, SymbolKind.NamedType);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
// 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.Diagnostics;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers
|
||||
{
|
||||
internal class UseAuthorizationAnalyzer
|
||||
{
|
||||
private readonly StartupAnalysis _context;
|
||||
|
||||
public UseAuthorizationAnalyzer(StartupAnalysis context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public void AnalyzeSymbol(SymbolAnalysisContext context)
|
||||
{
|
||||
Debug.Assert(context.Symbol.Kind == SymbolKind.NamedType);
|
||||
Debug.Assert(StartupFacts.IsStartupClass(_context.StartupSymbols, (INamedTypeSymbol)context.Symbol));
|
||||
|
||||
var type = (INamedTypeSymbol)context.Symbol;
|
||||
|
||||
foreach (var middlewareAnalysis in _context.GetRelatedAnalyses<MiddlewareAnalysis>(type))
|
||||
{
|
||||
MiddlewareItem? useAuthorizationItem = default;
|
||||
MiddlewareItem? useRoutingItem = default;
|
||||
|
||||
var length = middlewareAnalysis.Middleware.Length;
|
||||
for (var i = length - 1; i >= 0; i-- )
|
||||
{
|
||||
var middlewareItem = middlewareAnalysis.Middleware[i];
|
||||
var middleware = middlewareItem.UseMethod.Name;
|
||||
|
||||
if (middleware == "UseAuthorization")
|
||||
{
|
||||
if (useRoutingItem != null && useAuthorizationItem == null)
|
||||
{
|
||||
// This looks like
|
||||
//
|
||||
// app.UseAuthorization();
|
||||
// ...
|
||||
// app.UseRouting();
|
||||
// app.UseEndpoints(...);
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware,
|
||||
middlewareItem.Operation.Syntax.GetLocation(),
|
||||
middlewareItem.UseMethod.Name));
|
||||
}
|
||||
|
||||
useAuthorizationItem = middlewareItem;
|
||||
}
|
||||
else if (middleware == "UseEndpoints")
|
||||
{
|
||||
if (useAuthorizationItem != null)
|
||||
{
|
||||
// This configuration looks like
|
||||
//
|
||||
// app.UseRouting();
|
||||
// app.UseEndpoints(...);
|
||||
// ...
|
||||
// app.UseAuthorization();
|
||||
//
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware,
|
||||
useAuthorizationItem.Operation.Syntax.GetLocation(),
|
||||
middlewareItem.UseMethod.Name));
|
||||
}
|
||||
}
|
||||
else if (middleware == "UseRouting")
|
||||
{
|
||||
useRoutingItem = middlewareItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,7 +162,7 @@ namespace Microsoft.AspNetCore.Analyzers
|
|||
|
||||
Assert.Collection(
|
||||
middlewareAnalysis.Middleware,
|
||||
item => Assert.Equal("UseAuthorization", item.UseMethod.Name),
|
||||
item => Assert.Equal("UseStaticFiles", item.UseMethod.Name),
|
||||
item => Assert.Equal("UseMiddleware", item.UseMethod.Name),
|
||||
item => Assert.Equal("UseMvc", item.UseMethod.Name),
|
||||
item => Assert.Equal("UseRouting", item.UseMethod.Name),
|
||||
|
|
@ -228,5 +228,90 @@ namespace Microsoft.AspNetCore.Analyzers
|
|||
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic.Location);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartupAnalyzer_UseAuthorizationConfiguredCorrectly_ReportsNoDiagnostics()
|
||||
{
|
||||
// Arrange
|
||||
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthConfiguredCorrectly));
|
||||
|
||||
// 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()
|
||||
{
|
||||
// Arrange
|
||||
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthMultipleTimes));
|
||||
|
||||
// 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_UseAuthorizationConfiguredBeforeUseRouting_ReportsDiagnostics()
|
||||
{
|
||||
// Arrange
|
||||
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthBeforeUseRouting));
|
||||
|
||||
// Act
|
||||
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
|
||||
|
||||
// Assert
|
||||
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
|
||||
Assert.NotEmpty(middlewareAnalysis.Middleware);
|
||||
Assert.Collection(diagnostics,
|
||||
diagnostic =>
|
||||
{
|
||||
Assert.Same(StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware, diagnostic.Descriptor);
|
||||
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartupAnalyzer_UseAuthorizationConfiguredAfterUseEndpoints_ReportsDiagnostics()
|
||||
{
|
||||
// Arrange
|
||||
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthAfterUseEndpoints));
|
||||
|
||||
// Act
|
||||
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
|
||||
|
||||
// Assert
|
||||
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
|
||||
Assert.NotEmpty(middlewareAnalysis.Middleware);
|
||||
Assert.Collection(diagnostics,
|
||||
diagnostic =>
|
||||
{
|
||||
Assert.Same(StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware, diagnostic.Descriptor);
|
||||
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartupAnalyzer_MultipleUseAuthorization_ReportsNoDiagnostics()
|
||||
{
|
||||
// Arrange
|
||||
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthFallbackPolicy));
|
||||
|
||||
// Act
|
||||
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
|
||||
|
||||
// Assert
|
||||
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
|
||||
Assert.NotEmpty(middlewareAnalysis.Middleware);
|
||||
Assert.Empty(diagnostics);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.Authorization;
|
||||
|
|
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
|
|||
{
|
||||
/*MM1*/app.UseMvcWithDefaultRoute();
|
||||
|
||||
app.UseAuthorization();
|
||||
app.UseStaticFiles();
|
||||
app.UseMiddleware<AuthorizationMiddleware>();
|
||||
|
||||
/*MM2*/app.UseMvc();
|
||||
|
|
|
|||
|
|
@ -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.Authorization;
|
||||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
|
|||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseAuthorization();
|
||||
app.UseStaticFiles();
|
||||
app.UseMiddleware<AuthorizationMiddleware>();
|
||||
|
||||
/*MM*/app.UseMvc();
|
||||
|
|
|
|||
|
|
@ -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 UseAuthAfterUseEndpoints
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(r => { });
|
||||
/*MM*/app.UseAuthorization();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
|
||||
{
|
||||
public class UseAuthBeforeUseRouting
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseFileServer();
|
||||
/*MM*/app.UseAuthorization();
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(r => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 UseAuthConfiguredCorrectly
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(r => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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 UseAuthFallbackPolicy
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
// This sort of setup would be useful if the user wants to use Auth for non-endpoint content to be handled using the Fallback policy, while
|
||||
// using the second instance for regular endpoint routing based auth. We do not want to produce a warning in this case.
|
||||
app.UseAuthorization();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(r => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
|
||||
{
|
||||
public class UseAuthMultipleTimes
|
||||
{
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(r => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
|
||||
if (_subscribers != null && ChangeDetection.MayHaveChanged(previousValue, Value))
|
||||
{
|
||||
NotifySubscribers();
|
||||
NotifySubscribers(parameters.Lifetime);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -168,11 +168,11 @@ namespace Microsoft.AspNetCore.Components
|
|||
_subscribers.Remove(subscriber);
|
||||
}
|
||||
|
||||
private void NotifySubscribers()
|
||||
private void NotifySubscribers(in ParameterViewLifetime lifetime)
|
||||
{
|
||||
foreach (var subscriber in _subscribers)
|
||||
{
|
||||
subscriber.NotifyCascadingValueChanged();
|
||||
subscriber.NotifyCascadingValueChanged(lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Components.Reflection;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
|
|
@ -20,19 +21,21 @@ namespace Microsoft.AspNetCore.Components
|
|||
RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
|
||||
};
|
||||
|
||||
private static readonly ParameterView _empty = new ParameterView(_emptyFrames, 0, null);
|
||||
private static readonly ParameterView _empty = new ParameterView(ParameterViewLifetime.Unbound, _emptyFrames, 0, null);
|
||||
|
||||
private readonly ParameterViewLifetime _lifetime;
|
||||
private readonly RenderTreeFrame[] _frames;
|
||||
private readonly int _ownerIndex;
|
||||
private readonly IReadOnlyList<CascadingParameterState> _cascadingParametersOrNull;
|
||||
|
||||
internal ParameterView(RenderTreeFrame[] frames, int ownerIndex)
|
||||
: this(frames, ownerIndex, null)
|
||||
internal ParameterView(in ParameterViewLifetime lifetime, RenderTreeFrame[] frames, int ownerIndex)
|
||||
: this(lifetime, frames, ownerIndex, null)
|
||||
{
|
||||
}
|
||||
|
||||
private ParameterView(RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList<CascadingParameterState> cascadingParametersOrNull)
|
||||
private ParameterView(in ParameterViewLifetime lifetime, RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList<CascadingParameterState> cascadingParametersOrNull)
|
||||
{
|
||||
_lifetime = lifetime;
|
||||
_frames = frames;
|
||||
_ownerIndex = ownerIndex;
|
||||
_cascadingParametersOrNull = cascadingParametersOrNull;
|
||||
|
|
@ -43,12 +46,17 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// </summary>
|
||||
public static ParameterView Empty => _empty;
|
||||
|
||||
internal ParameterViewLifetime Lifetime => _lifetime;
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerator that iterates through the <see cref="ParameterView"/>.
|
||||
/// </summary>
|
||||
/// <returns>The enumerator.</returns>
|
||||
public Enumerator GetEnumerator()
|
||||
=> new Enumerator(_frames, _ownerIndex, _cascadingParametersOrNull);
|
||||
{
|
||||
_lifetime.AssertNotExpired();
|
||||
return new Enumerator(_frames, _ownerIndex, _cascadingParametersOrNull);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the parameter with the specified name.
|
||||
|
|
@ -108,7 +116,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
}
|
||||
|
||||
internal ParameterView WithCascadingParameters(IReadOnlyList<CascadingParameterState> cascadingParameters)
|
||||
=> new ParameterView(_frames, _ownerIndex, cascadingParameters);
|
||||
=> new ParameterView(_lifetime, _frames, _ownerIndex, cascadingParameters);
|
||||
|
||||
// It's internal because there isn't a known use case for user code comparing
|
||||
// ParameterView instances, and even if there was, it's unlikely it should
|
||||
|
|
@ -215,7 +223,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
frames[++i] = RenderTreeFrame.Attribute(i, kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
return new ParameterView(frames, 0);
|
||||
return new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -364,8 +364,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
// Handles the diff for attribute nodes only - this invariant is enforced by the caller.
|
||||
//
|
||||
// The diff for attributes is different because we allow attributes to appear in any order.
|
||||
// Put another way, the attributes list of an element or component is *conceptually*
|
||||
// unordered. This is a case where we can produce a more minimal diff by avoiding
|
||||
// Put another way, the attributes list of an element or component is *conceptually*
|
||||
// unordered. This is a case where we can produce a more minimal diff by avoiding
|
||||
// non-meaningful reorderings of attributes.
|
||||
private static void AppendAttributeDiffEntriesForRange(
|
||||
ref DiffContext diffContext,
|
||||
|
|
@ -519,8 +519,9 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
// comparisons it wants with the old values. Later we could choose to pass the
|
||||
// old parameter values if we wanted. By default, components always rerender
|
||||
// after any SetParameters call, which is safe but now always optimal for perf.
|
||||
var oldParameters = new ParameterView(oldTree, oldComponentIndex);
|
||||
var newParameters = new ParameterView(newTree, newComponentIndex);
|
||||
var oldParameters = new ParameterView(ParameterViewLifetime.Unbound, oldTree, oldComponentIndex);
|
||||
var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
|
||||
var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex);
|
||||
if (!newParameters.DefinitelyEquals(oldParameters))
|
||||
{
|
||||
componentState.SetDirectParameters(newParameters);
|
||||
|
|
@ -893,7 +894,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
var childComponentState = frame.ComponentState;
|
||||
|
||||
// Set initial parameters
|
||||
var initialParameters = new ParameterView(frames, frameIndex);
|
||||
var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
|
||||
var initialParameters = new ParameterView(initialParametersLifetime, frames, frameIndex);
|
||||
childComponentState.SetDirectParameters(initialParameters);
|
||||
}
|
||||
|
||||
|
|
@ -957,7 +959,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
/// Exists only so that the various methods in this class can call each other without
|
||||
/// constantly building up long lists of parameters. Is private to this class, so the
|
||||
/// fact that it's a mutable struct is manageable.
|
||||
///
|
||||
///
|
||||
/// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable.
|
||||
/// </summary>
|
||||
private struct DiffContext
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
_renderTreeBuilderPrevious.GetFrames(),
|
||||
CurrentRenderTree.GetFrames());
|
||||
batchBuilder.UpdatedComponentDiffs.Append(diff);
|
||||
batchBuilder.InvalidateParameterViews();
|
||||
}
|
||||
|
||||
public bool TryDisposeInBatch(RenderBatchBuilder batchBuilder, out Exception exception)
|
||||
|
|
@ -130,8 +131,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void SetDirectParameters(ParameterView parameters)
|
||||
{
|
||||
// Note: We should be careful to ensure that the framework never calls
|
||||
// IComponent.SetParameters directly elsewhere. We should only call it
|
||||
// via ComponentState.SetParameters (or NotifyCascadingValueChanged below).
|
||||
// IComponent.SetParametersAsync directly elsewhere. We should only call it
|
||||
// via ComponentState.SetDirectParameters (or NotifyCascadingValueChanged below).
|
||||
// If we bypass this, the component won't receive the cascading parameters nor
|
||||
// will it update its snapshot of direct parameters.
|
||||
|
||||
|
|
@ -156,10 +157,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
_renderer.AddToPendingTasks(Component.SetParametersAsync(parameters));
|
||||
}
|
||||
|
||||
public void NotifyCascadingValueChanged()
|
||||
public void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime)
|
||||
{
|
||||
var directParams = _latestDirectParametersSnapshot != null
|
||||
? new ParameterView(_latestDirectParametersSnapshot.Buffer, 0)
|
||||
? new ParameterView(lifetime, _latestDirectParametersSnapshot.Buffer, 0)
|
||||
: ParameterView.Empty;
|
||||
var allParams = directParams.WithCascadingParameters(_cascadingParameters);
|
||||
var task = Component.SetParametersAsync(allParams);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// 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.Components.Rendering
|
||||
{
|
||||
internal readonly struct ParameterViewLifetime
|
||||
{
|
||||
private readonly RenderBatchBuilder _owner;
|
||||
private readonly int _stamp;
|
||||
|
||||
public static readonly ParameterViewLifetime Unbound = default;
|
||||
|
||||
public ParameterViewLifetime(RenderBatchBuilder owner)
|
||||
{
|
||||
_owner = owner;
|
||||
_stamp = owner.ParameterViewValidityStamp;
|
||||
}
|
||||
|
||||
public void AssertNotExpired()
|
||||
{
|
||||
// If _owner is null, this instance is default(ParameterViewLifetime), which is
|
||||
// the same as ParameterViewLifetime.Unbound. That means it never expires.
|
||||
if (_owner != null && _owner.ParameterViewValidityStamp != _stamp)
|
||||
{
|
||||
throw new InvalidOperationException($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </summary>
|
||||
internal class RenderBatchBuilder : IDisposable
|
||||
{
|
||||
// A value that, if changed, causes expiry of all ParameterView instances issued
|
||||
// for this RenderBatchBuilder. This is to prevent invalid reads from arrays that
|
||||
// may have been returned to the shared pool.
|
||||
private int _parameterViewValidityStamp;
|
||||
|
||||
// Primary result data
|
||||
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
|
||||
public ArrayBuilder<int> DisposedComponentIds { get; } = new ArrayBuilder<int>();
|
||||
|
|
@ -31,6 +36,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
// Scratch data structure for understanding attribute diffs.
|
||||
public Dictionary<string, int> AttributeDiffSet { get; } = new Dictionary<string, int>();
|
||||
|
||||
public int ParameterViewValidityStamp => _parameterViewValidityStamp;
|
||||
|
||||
internal StackObjectPool<Dictionary<object, KeyedItemInfo>> KeyedItemInfoDictionaryPool { get; }
|
||||
= new StackObjectPool<Dictionary<object, KeyedItemInfo>>(maxPreservedItems: 10, () => new Dictionary<object, KeyedItemInfo>());
|
||||
|
||||
|
|
@ -58,6 +65,22 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
DisposedComponentIds.ToRange(),
|
||||
DisposedEventHandlerIds.ToRange());
|
||||
|
||||
public void InvalidateParameterViews()
|
||||
{
|
||||
// Wrapping is fine because all that matters is whether a snapshotted value matches
|
||||
// the current one. There's no plausible case where it wraps around and happens to
|
||||
// increment all the way back to a previously-snapshotted value on the exact same
|
||||
// call that's checking the value.
|
||||
if (_parameterViewValidityStamp == int.MaxValue)
|
||||
{
|
||||
_parameterViewValidityStamp = int.MinValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
_parameterViewValidityStamp++;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
EditsBuffer.Dispose();
|
||||
|
|
|
|||
|
|
@ -354,6 +354,40 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
Assert.Equal("The value of IsFixed cannot be changed dynamically.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParameterViewSuppliedWithCascadingParametersCannotBeUsedAfterSynchronousReturn()
|
||||
{
|
||||
// Arrange
|
||||
var providedValue = "Initial value";
|
||||
var renderer = new TestRenderer();
|
||||
var component = new TestComponent(builder =>
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<string>>(0);
|
||||
builder.AddAttribute(1, "Value", providedValue);
|
||||
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
|
||||
{
|
||||
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
|
||||
childBuilder.CloseComponent();
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
});
|
||||
|
||||
// Initial render; capture nested component
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
component.TriggerRender();
|
||||
var firstBatch = renderer.Batches.Single();
|
||||
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
|
||||
|
||||
// Re-render CascadingValue with new value, so it gets a new ParameterView
|
||||
providedValue = "Updated value";
|
||||
component.TriggerRender();
|
||||
Assert.Equal(2, renderer.Batches.Count);
|
||||
|
||||
// It's no longer able to access anything in the ParameterView it just received
|
||||
var ex = Assert.Throws<InvalidOperationException>(nestedComponent.AttemptIllegalAccessToLastParameterView);
|
||||
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
|
||||
}
|
||||
|
||||
private static T FindComponent<T>(CapturedBatch batch, out int componentId)
|
||||
{
|
||||
var componentFrame = batch.ReferenceFrames.Single(
|
||||
|
|
@ -378,6 +412,8 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
class CascadingParameterConsumerComponent<T> : AutoRenderComponent
|
||||
{
|
||||
private ParameterView lastParameterView;
|
||||
|
||||
public int NumSetParametersCalls { get; private set; }
|
||||
public int NumRenders { get; private set; }
|
||||
|
||||
|
|
@ -386,6 +422,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
public override async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
lastParameterView = parameters;
|
||||
NumSetParametersCalls++;
|
||||
await base.SetParametersAsync(parameters);
|
||||
}
|
||||
|
|
@ -395,6 +432,13 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
NumRenders++;
|
||||
builder.AddContent(0, $"CascadingParameter={CascadingParameter}; RegularParameter={RegularParameter}");
|
||||
}
|
||||
|
||||
public void AttemptIllegalAccessToLastParameterView()
|
||||
{
|
||||
// You're not allowed to hold onto a ParameterView and access it later,
|
||||
// so this should throw
|
||||
lastParameterView.TryGetValue<object>("anything", out _);
|
||||
}
|
||||
}
|
||||
|
||||
class SecondCascadingParameterConsumerComponent<T1, T2> : CascadingParameterConsumerComponent<T1>
|
||||
|
|
|
|||
|
|
@ -673,7 +673,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
}
|
||||
builder.CloseComponent();
|
||||
|
||||
var view = new ParameterView(builder.GetFrames().Array, ownerIndex: 0);
|
||||
var view = new ParameterView(ParameterViewLifetime.Unbound, builder.GetFrames().Array, ownerIndex: 0);
|
||||
|
||||
var cascadingParameters = new List<CascadingParameterState>();
|
||||
foreach (var kvp in _keyValuePairs)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
RenderTreeFrame.ChildComponent(0, typeof(FakeComponent)).WithComponentSubtreeLength(1)
|
||||
};
|
||||
var parameters = new ParameterView(frames, 0);
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(ToEnumerable(parameters));
|
||||
|
|
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(1)
|
||||
};
|
||||
var parameters = new ParameterView(frames, 0);
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(ToEnumerable(parameters));
|
||||
|
|
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
// end of the owner's descendants
|
||||
RenderTreeFrame.Attribute(3, "orphaned attribute", "value")
|
||||
};
|
||||
var parameters = new ParameterView(frames, 0);
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(ToEnumerable(parameters),
|
||||
|
|
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
RenderTreeFrame.Element(3, "child element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(4, "child attribute", "some value")
|
||||
};
|
||||
var parameters = new ParameterView(frames, 0);
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(ToEnumerable(parameters),
|
||||
|
|
@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
var attribute1Value = new object();
|
||||
var attribute2Value = new object();
|
||||
var attribute3Value = new object();
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value)
|
||||
|
|
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
public void CanTryGetNonExistingValue()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
||||
|
|
@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
public void CanTryGetExistingValueWithCorrectType()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(1, "my entry", "hello")
|
||||
|
|
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var myEntryValue = new object();
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
|
||||
|
|
@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var myEntryValue = new object();
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
|
||||
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
|
||||
|
|
@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
public void CanGetValueOrDefault_WithNonExistingValue()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
||||
|
|
@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var explicitDefaultValue = new DateTime(2018, 3, 20);
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
||||
|
|
@ -226,7 +226,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
public void ThrowsIfTryGetExistingValueWithIncorrectType()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(1, "my entry", "hello")
|
||||
|
|
@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var entry2Value = new object();
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
|
||||
RenderTreeFrame.Attribute(0, "entry 1", "value 1"),
|
||||
|
|
@ -304,7 +304,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var myEntryValue = new object();
|
||||
var parameters = new ParameterView(new[]
|
||||
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||
RenderTreeFrame.Attribute(1, "unrelated value", new object())
|
||||
|
|
@ -322,6 +322,32 @@ namespace Microsoft.AspNetCore.Components
|
|||
Assert.Same(myEntryValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotReadAfterLifetimeExpiry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new RenderBatchBuilder();
|
||||
var lifetime = new ParameterViewLifetime(builder);
|
||||
var frames = new[]
|
||||
{
|
||||
RenderTreeFrame.ChildComponent(0, typeof(FakeComponent)).WithComponentSubtreeLength(1)
|
||||
};
|
||||
var parameterView = new ParameterView(lifetime, frames, 0);
|
||||
|
||||
// Act
|
||||
builder.InvalidateParameterViews();
|
||||
|
||||
// Assert
|
||||
Assert.Throws<InvalidOperationException>(() => parameterView.GetEnumerator());
|
||||
Assert.Throws<InvalidOperationException>(() => parameterView.GetValueOrDefault<object>("anything"));
|
||||
Assert.Throws<InvalidOperationException>(() => parameterView.SetParameterProperties(new object()));
|
||||
Assert.Throws<InvalidOperationException>(() => parameterView.ToDictionary());
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => parameterView.TryGetValue<object>("anything", out _));
|
||||
|
||||
// It's enough to assert about one of the messages
|
||||
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
|
||||
}
|
||||
|
||||
private Action<ParameterValue> AssertParameter(string expectedName, object expectedValue, bool expectedIsCascading)
|
||||
{
|
||||
return parameter =>
|
||||
|
|
|
|||
|
|
@ -3701,6 +3701,39 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
Assert.Contains("Cannot start a batch when one is already in progress.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotAccessParameterViewAfterSynchronousReturn()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var rootComponent = new TestComponent(builder =>
|
||||
{
|
||||
builder.OpenComponent<ParameterViewIllegalCapturingComponent>(0);
|
||||
builder.AddAttribute(1, nameof(ParameterViewIllegalCapturingComponent.SomeParam), 0);
|
||||
builder.CloseComponent();
|
||||
});
|
||||
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
|
||||
|
||||
// Note that we're not waiting for the async render to complete, since we want to assert
|
||||
// about the situation immediately after the component yields the thread
|
||||
renderer.RenderRootComponentAsync(rootComponentId);
|
||||
|
||||
// Act/Assert
|
||||
var capturingComponent = (ParameterViewIllegalCapturingComponent)renderer.GetCurrentRenderTreeFrames(rootComponentId).Array[0].Component;
|
||||
var parameterView = capturingComponent.CapturedParameterView;
|
||||
|
||||
// All public APIs on capturingComponent should be electrified now
|
||||
// Internal APIs don't have to be, because we won't call them at the wrong time
|
||||
Assert.Throws<InvalidOperationException>(() => parameterView.GetEnumerator());
|
||||
Assert.Throws<InvalidOperationException>(() => parameterView.GetValueOrDefault<object>("anything"));
|
||||
Assert.Throws<InvalidOperationException>(() => parameterView.SetParameterProperties(new object()));
|
||||
Assert.Throws<InvalidOperationException>(() => parameterView.ToDictionary());
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => parameterView.TryGetValue<object>("anything", out _));
|
||||
|
||||
// It's enough to assert about one of the messages
|
||||
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
|
||||
}
|
||||
|
||||
private class NoOpRenderer : Renderer
|
||||
{
|
||||
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
|
||||
|
|
@ -4443,5 +4476,25 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
public new void ProcessPendingRender()
|
||||
=> base.ProcessPendingRender();
|
||||
}
|
||||
|
||||
class ParameterViewIllegalCapturingComponent : IComponent
|
||||
{
|
||||
public ParameterView CapturedParameterView { get; private set; }
|
||||
|
||||
[Parameter] public int SomeParam { get; set; }
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
CapturedParameterView = parameters;
|
||||
|
||||
// Return a task that never completes to show that access is forbidden
|
||||
// after the synchronous return, not just after the returned task completes
|
||||
return new TaskCompletionSource<object>().Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
var count = Descriptors.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var (componentType, sequence) = Descriptors[i];
|
||||
await Renderer.AddComponentAsync(componentType, sequence.ToString());
|
||||
var (componentType, parameters, sequence) = Descriptors[i];
|
||||
await Renderer.AddComponentAsync(componentType, parameters, sequence.ToString());
|
||||
}
|
||||
|
||||
Log.InitializationSucceeded(_logger);
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
{
|
||||
public Type ComponentType { get; set; }
|
||||
|
||||
public ParameterView Parameters { get; set; }
|
||||
|
||||
public int Sequence { get; set; }
|
||||
|
||||
public void Deconstruct(out Type componentType, out int sequence)
|
||||
{
|
||||
componentType = ComponentType;
|
||||
sequence = Sequence;
|
||||
}
|
||||
public void Deconstruct(out Type componentType, out ParameterView parameters, out int sequence) =>
|
||||
(componentType, sequence, parameters) = (ComponentType, Sequence, Parameters);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
// 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.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server
|
||||
{
|
||||
internal class ComponentParameterDeserializer
|
||||
{
|
||||
private readonly ILogger<ComponentParameterDeserializer> _logger;
|
||||
private readonly ComponentParametersTypeCache _parametersCache;
|
||||
|
||||
public ComponentParameterDeserializer(
|
||||
ILogger<ComponentParameterDeserializer> logger,
|
||||
ComponentParametersTypeCache parametersCache)
|
||||
{
|
||||
_logger = logger;
|
||||
_parametersCache = parametersCache;
|
||||
}
|
||||
|
||||
public bool TryDeserializeParameters(IList<ComponentParameter> parametersDefinitions, IList<object> parameterValues, out ParameterView parameters)
|
||||
{
|
||||
parameters = default;
|
||||
var parametersDictionary = new Dictionary<string, object>();
|
||||
|
||||
if (parameterValues.Count != parametersDefinitions.Count)
|
||||
{
|
||||
// Mismatched number of definition/parameter values.
|
||||
Log.MismatchedParameterAndDefinitions(_logger, parametersDefinitions.Count, parameterValues.Count);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < parametersDefinitions.Count; i++)
|
||||
{
|
||||
var definition = parametersDefinitions[i];
|
||||
if (definition.Name == null)
|
||||
{
|
||||
Log.MissingParameterDefinitionName(_logger);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.TypeName == null && definition.Assembly == null)
|
||||
{
|
||||
parametersDictionary.Add(definition.Name, null);
|
||||
}
|
||||
else if (definition.TypeName == null || definition.Assembly == null)
|
||||
{
|
||||
Log.IncompleteParameterDefinition(_logger, definition.Name, definition.TypeName, definition.Assembly);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var parameterType = _parametersCache.GetParameterType(definition.Assembly, definition.TypeName);
|
||||
if (parameterType == null)
|
||||
{
|
||||
Log.InvalidParameterType(_logger, definition.Name, definition.Assembly, definition.TypeName);
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
// At this point we know the parameter is not null, as we don't serialize the type name or the assembly name
|
||||
// for null parameters.
|
||||
var value = (JsonElement)parameterValues[i];
|
||||
var parameterValue = JsonSerializer.Deserialize(
|
||||
value.GetRawText(),
|
||||
parameterType,
|
||||
ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
|
||||
parametersDictionary.Add(definition.Name, parameterValue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.InvalidParameterValue(_logger, definition.Name, definition.TypeName, definition.Assembly, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameters = ParameterView.FromDictionary(parametersDictionary);
|
||||
return true;
|
||||
}
|
||||
|
||||
private ComponentParameter[] GetParameterDefinitions(string parametersDefinitions)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<ComponentParameter[]>(parametersDefinitions, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.FailedToParseParameterDefinitions(_logger, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private JsonDocument GetParameterValues(string parameterValues)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonDocument.Parse(parameterValues);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.FailedToParseParameterValues(_logger, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, Exception> _parameterValuesInvalidFormat =
|
||||
LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
new EventId(1, "ParameterValuesInvalidFormat"),
|
||||
"Parameter values must be an array.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, Exception> _incompleteParameterDefinition =
|
||||
LoggerMessage.Define<string, string, string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(2, "IncompleteParameterDefinition"),
|
||||
"The parameter definition for '{ParameterName}' is incomplete: Type='{TypeName}' Assembly='{Assembly}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, Exception> _invalidParameterType =
|
||||
LoggerMessage.Define<string, string, string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(3, "InvalidParameterType"),
|
||||
"The parameter '{ParameterName} with type '{TypeName}' in assembly '{Assembly}' could not be found.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, Exception> _invalidParameterValue =
|
||||
LoggerMessage.Define<string, string, string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4, "InvalidParameterValue"),
|
||||
"Could not parse the parameter value for parameter '{Name}' of type '{TypeName}' and assembly '{Assembly}'.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _failedToParseParameterDefinitions =
|
||||
LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
new EventId(5, "FailedToParseParameterDefinitions"),
|
||||
"Failed to parse the parameter definitions.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _failedToParseParameterValues =
|
||||
LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
new EventId(6, "FailedToParseParameterValues"),
|
||||
"Failed to parse the parameter values.");
|
||||
|
||||
private static readonly Action<ILogger, int, int, Exception> _mismatchedParameterAndDefinitions =
|
||||
LoggerMessage.Define<int, int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(7, "MismatchedParameterAndDefinitions"),
|
||||
"The number of parameter definitions '{DescriptorsLength}' does not match the number parameter values '{ValuesLength}'.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _missingParameterDefinitionName =
|
||||
LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
new EventId(8, "MissingParameterDefinitionName"),
|
||||
"The name is missing in a parameter definition.");
|
||||
|
||||
internal static void ParameterValuesInvalidFormat(ILogger<ComponentParameterDeserializer> logger) =>
|
||||
_parameterValuesInvalidFormat(logger, null);
|
||||
|
||||
internal static void IncompleteParameterDefinition(ILogger<ComponentParameterDeserializer> logger, string name, string typeName, string assembly) =>
|
||||
_incompleteParameterDefinition(logger, name, typeName, assembly, null);
|
||||
|
||||
internal static void InvalidParameterType(ILogger<ComponentParameterDeserializer> logger, string name, string assembly, string typeName) =>
|
||||
_invalidParameterType(logger, name, assembly, typeName, null);
|
||||
|
||||
internal static void InvalidParameterValue(ILogger<ComponentParameterDeserializer> logger, string name, string typeName, string assembly, Exception e) =>
|
||||
_invalidParameterValue(logger, name, typeName, assembly,e);
|
||||
|
||||
internal static void FailedToParseParameterDefinitions(ILogger<ComponentParameterDeserializer> logger, Exception e) =>
|
||||
_failedToParseParameterDefinitions(logger, e);
|
||||
|
||||
internal static void FailedToParseParameterValues(ILogger<ComponentParameterDeserializer> logger, Exception e) =>
|
||||
_failedToParseParameterValues(logger, e);
|
||||
|
||||
internal static void MismatchedParameterAndDefinitions(ILogger<ComponentParameterDeserializer> logger, int definitionsLength, int valuesLength) =>
|
||||
_mismatchedParameterAndDefinitions(logger, definitionsLength, valuesLength, null);
|
||||
|
||||
internal static void MissingParameterDefinitionName(ILogger<ComponentParameterDeserializer> logger) =>
|
||||
_missingParameterDefinitionName(logger, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// 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.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
internal class ComponentParametersTypeCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
|
||||
|
||||
public Type GetParameterType(string assembly, string type)
|
||||
{
|
||||
var key = new Key(assembly, type);
|
||||
if (_typeToKeyLookUp.TryGetValue(key, out var resolvedType))
|
||||
{
|
||||
return resolvedType;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _typeToKeyLookUp.GetOrAdd(key, ResolveType, AppDomain.CurrentDomain.GetAssemblies());
|
||||
}
|
||||
}
|
||||
|
||||
private static Type ResolveType(Key key, Assembly[] assemblies)
|
||||
{
|
||||
var assembly = assemblies
|
||||
.FirstOrDefault(a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal));
|
||||
|
||||
if (assembly == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
|
||||
}
|
||||
|
||||
private struct Key : IEquatable<Key>
|
||||
{
|
||||
public Key(string assembly, string type) =>
|
||||
(Assembly, Type) = (assembly, type);
|
||||
|
||||
public string Assembly { get; set; }
|
||||
|
||||
public string Type { get; set; }
|
||||
|
||||
public override bool Equals(object obj) => Equals((Key)obj);
|
||||
|
||||
public bool Equals(Key other) => string.Equals(Assembly, other.Assembly, StringComparison.Ordinal) &&
|
||||
string.Equals(Type, other.Type, StringComparison.Ordinal);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Assembly, Type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,12 +70,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
if (!_clientProxy.Connected)
|
||||
if (_clientProxy is null)
|
||||
{
|
||||
throw new InvalidOperationException("JavaScript interop calls cannot be issued at this time. This is because the component is being " +
|
||||
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
|
||||
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not " +
|
||||
"attempted during prerendering or while the client is disconnected.");
|
||||
throw new InvalidOperationException(
|
||||
"JavaScript interop calls cannot be issued at this time. This is because the component is being " +
|
||||
$"statically rendererd. When prerendering is enabled, JavaScript interop calls can only be performed " +
|
||||
$"during the OnAfterRenderAsync lifecycle method.");
|
||||
}
|
||||
|
||||
Log.BeginInvokeJS(_logger, asyncHandle, identifier);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
return RenderRootComponentAsync(componentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Associates the <see cref="IComponent"/> with the <see cref="RemoteRenderer"/>,
|
||||
/// causing it to be displayed in the specified DOM element.
|
||||
/// </summary>
|
||||
/// <param name="componentType">The type of the component.</param>
|
||||
/// <param name="parameters">The parameters for the component.</param>
|
||||
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
||||
public Task AddComponentAsync(Type componentType, ParameterView parameters, string domElementSelector)
|
||||
{
|
||||
var component = InstantiateComponent(componentType);
|
||||
var componentId = AssignRootComponentId(component);
|
||||
|
||||
var attachComponentTask = _client.SendAsync("JS.AttachComponent", componentId, domElementSelector);
|
||||
CaptureAsyncExceptions(attachComponentTask);
|
||||
|
||||
return RenderRootComponentAsync(componentId, parameters);
|
||||
}
|
||||
|
||||
protected override void ProcessPendingRender()
|
||||
{
|
||||
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,12 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
// 'sequence' indicates the order in which this component got rendered on the server.
|
||||
// 'assemblyName' the assembly name for the rendered component.
|
||||
// 'type' the full type name for the rendered component.
|
||||
// 'parameterDefinitions' a JSON serialized array that contains the definitions for the parameters including their names and types and assemblies.
|
||||
// 'parameterValues' a JSON serialized array containing the parameter values.
|
||||
// 'invocationId' a random string that matches all components rendered by as part of a single HTTP response.
|
||||
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>"}))
|
||||
// With parameters
|
||||
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>", parameterDefinitions: "[{ \"name\":\"Parameter\", \"typeName\":\"string\", \"assembly\":\"System.Private.CoreLib\"}], parameterValues: [<<string-value>>]}))
|
||||
|
||||
// Serialization:
|
||||
// For a given response, MVC renders one or more markers in sequence, including a descriptor for each rendered
|
||||
|
|
@ -55,11 +59,13 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
private readonly IDataProtector _dataProtector;
|
||||
private readonly ILogger<ServerComponentDeserializer> _logger;
|
||||
private readonly ServerComponentTypeCache _rootComponentTypeCache;
|
||||
private readonly ComponentParameterDeserializer _parametersDeserializer;
|
||||
|
||||
public ServerComponentDeserializer(
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<ServerComponentDeserializer> logger,
|
||||
ServerComponentTypeCache rootComponentTypeCache)
|
||||
ServerComponentTypeCache rootComponentTypeCache,
|
||||
ComponentParameterDeserializer parametersDeserializer)
|
||||
{
|
||||
// When we protect the data we use a time-limited data protector with the
|
||||
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
|
||||
|
|
@ -74,6 +80,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
|
||||
_logger = logger;
|
||||
_rootComponentTypeCache = rootComponentTypeCache;
|
||||
_parametersDeserializer = parametersDeserializer;
|
||||
}
|
||||
|
||||
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
|
||||
|
|
@ -176,9 +183,16 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
return default;
|
||||
}
|
||||
|
||||
if (!_parametersDeserializer.TryDeserializeParameters(serverComponent.ParameterDefinitions, serverComponent.ParameterValues, out var parameters))
|
||||
{
|
||||
// TryDeserializeParameters does appropriate logging.
|
||||
return default;
|
||||
}
|
||||
|
||||
var componentDescriptor = new ComponentDescriptor
|
||||
{
|
||||
ComponentType = componentType,
|
||||
Parameters = parameters,
|
||||
Sequence = serverComponent.Sequence
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<CircuitFactory>();
|
||||
services.TryAddSingleton<ServerComponentDeserializer>();
|
||||
services.TryAddSingleton<ServerComponentTypeCache>();
|
||||
services.TryAddSingleton<ComponentParameterDeserializer>();
|
||||
services.TryAddSingleton<ComponentParametersTypeCache>();
|
||||
services.TryAddSingleton<CircuitIdFactory>();
|
||||
|
||||
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
|
||||
<!-- Shared descriptor infrastructure with MVC -->
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -40,6 +41,45 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
Assert.Equal(0, deserializedDescriptor.Sequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseSingleMarkerWithParameters()
|
||||
{
|
||||
// Arrange
|
||||
var markers = SerializeMarkers(CreateMarkers(
|
||||
(typeof(TestComponent), new Dictionary<string, object> { ["Parameter"] = "Value" })));
|
||||
var serverComponentDeserializer = CreateServerComponentDeserializer();
|
||||
|
||||
// Act & assert
|
||||
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
|
||||
var deserializedDescriptor = Assert.Single(descriptors);
|
||||
Assert.Equal(typeof(TestComponent).FullName, deserializedDescriptor.ComponentType.FullName);
|
||||
Assert.Equal(0, deserializedDescriptor.Sequence);
|
||||
var parameters = deserializedDescriptor.Parameters.ToDictionary();
|
||||
Assert.Single(parameters);
|
||||
Assert.Contains("Parameter", parameters.Keys);
|
||||
Assert.Equal("Value", parameters["Parameter"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseSingleMarkerWithNullParameters()
|
||||
{
|
||||
// Arrange
|
||||
var markers = SerializeMarkers(CreateMarkers(
|
||||
(typeof(TestComponent), new Dictionary<string, object> { ["Parameter"] = null })));
|
||||
var serverComponentDeserializer = CreateServerComponentDeserializer();
|
||||
|
||||
// Act & assert
|
||||
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
|
||||
var deserializedDescriptor = Assert.Single(descriptors);
|
||||
Assert.Equal(typeof(TestComponent).FullName, deserializedDescriptor.ComponentType.FullName);
|
||||
Assert.Equal(0, deserializedDescriptor.Sequence);
|
||||
|
||||
var parameters = deserializedDescriptor.Parameters.ToDictionary();
|
||||
Assert.Single(parameters);
|
||||
Assert.Contains("Parameter", parameters.Keys);
|
||||
Assert.Null(parameters["Parameter"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseMultipleMarkers()
|
||||
{
|
||||
|
|
@ -60,6 +100,65 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
Assert.Equal(1, secondDescriptor.Sequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseMultipleMarkersWithParameters()
|
||||
{
|
||||
// Arrange
|
||||
var markers = SerializeMarkers(CreateMarkers(
|
||||
(typeof(TestComponent), new Dictionary<string, object> { ["First"] = "Value" }),
|
||||
(typeof(TestComponent), new Dictionary<string, object> { ["Second"] = null })));
|
||||
var serverComponentDeserializer = CreateServerComponentDeserializer();
|
||||
|
||||
// Act & assert
|
||||
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
|
||||
Assert.Equal(2, descriptors.Count);
|
||||
|
||||
var firstDescriptor = descriptors[0];
|
||||
Assert.Equal(typeof(TestComponent).FullName, firstDescriptor.ComponentType.FullName);
|
||||
Assert.Equal(0, firstDescriptor.Sequence);
|
||||
var firstParameters = firstDescriptor.Parameters.ToDictionary();
|
||||
Assert.Single(firstParameters);
|
||||
Assert.Contains("First", firstParameters.Keys);
|
||||
Assert.Equal("Value", firstParameters["First"]);
|
||||
|
||||
|
||||
var secondDescriptor = descriptors[1];
|
||||
Assert.Equal(typeof(TestComponent).FullName, secondDescriptor.ComponentType.FullName);
|
||||
Assert.Equal(1, secondDescriptor.Sequence);
|
||||
var secondParameters = secondDescriptor.Parameters.ToDictionary();
|
||||
Assert.Single(secondParameters);
|
||||
Assert.Contains("Second", secondParameters.Keys);
|
||||
Assert.Null(secondParameters["Second"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseMultipleMarkersWithAndWithoutParameters()
|
||||
{
|
||||
// Arrange
|
||||
var markers = SerializeMarkers(CreateMarkers(
|
||||
(typeof(TestComponent), new Dictionary<string, object> { ["First"] = "Value" }),
|
||||
(typeof(TestComponent), null)));
|
||||
var serverComponentDeserializer = CreateServerComponentDeserializer();
|
||||
|
||||
// Act & assert
|
||||
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
|
||||
Assert.Equal(2, descriptors.Count);
|
||||
|
||||
var firstDescriptor = descriptors[0];
|
||||
Assert.Equal(typeof(TestComponent).FullName, firstDescriptor.ComponentType.FullName);
|
||||
Assert.Equal(0, firstDescriptor.Sequence);
|
||||
var firstParameters = firstDescriptor.Parameters.ToDictionary();
|
||||
Assert.Single(firstParameters);
|
||||
Assert.Contains("First", firstParameters.Keys);
|
||||
Assert.Equal("Value", firstParameters["First"]);
|
||||
|
||||
|
||||
var secondDescriptor = descriptors[1];
|
||||
Assert.Equal(typeof(TestComponent).FullName, secondDescriptor.ComponentType.FullName);
|
||||
Assert.Equal(1, secondDescriptor.Sequence);
|
||||
Assert.Empty(secondDescriptor.Parameters.ToDictionary());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotParseOutOfOrderMarkers()
|
||||
{
|
||||
|
|
@ -213,7 +312,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
private string SerializeComponent(string assembly, string type) =>
|
||||
JsonSerializer.Serialize(
|
||||
new ServerComponent(0, assembly, type, Guid.NewGuid()),
|
||||
new ServerComponent(0, assembly, type, Array.Empty<ComponentParameter>(), Array.Empty<object>(), Guid.NewGuid()),
|
||||
ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
|
||||
private ServerComponentDeserializer CreateServerComponentDeserializer()
|
||||
|
|
@ -221,7 +320,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
return new ServerComponentDeserializer(
|
||||
_ephemeralDataProtectionProvider,
|
||||
NullLogger<ServerComponentDeserializer>.Instance,
|
||||
new ServerComponentTypeCache());
|
||||
new ServerComponentTypeCache(),
|
||||
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
|
||||
}
|
||||
|
||||
private string SerializeMarkers(ServerComponentMarker[] markers) =>
|
||||
|
|
@ -233,7 +333,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
var markers = new ServerComponentMarker[types.Length];
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], false);
|
||||
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], ParameterView.Empty, false);
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
private ServerComponentMarker[] CreateMarkers(params (Type, Dictionary<string,object>)[] types)
|
||||
{
|
||||
var serializer = new ServerComponentSerializer(_ephemeralDataProtectionProvider);
|
||||
var markers = new ServerComponentMarker[types.Length];
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var (type, parameters) = types[i];
|
||||
markers[i] = serializer.SerializeInvocation(
|
||||
_invocationSequence,
|
||||
type,
|
||||
parameters == null ? ParameterView.Empty : ParameterView.FromDictionary(parameters),
|
||||
false);
|
||||
}
|
||||
|
||||
return markers;
|
||||
|
|
@ -245,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
var markers = new ServerComponentMarker[types.Length];
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
markers[i] = serializer.SerializeInvocation(sequence, types[i], false);
|
||||
markers[i] = serializer.SerializeInvocation(sequence, types[i], ParameterView.Empty, false);
|
||||
}
|
||||
|
||||
return markers;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -25,10 +25,14 @@ export const monoPlatform: Platform = {
|
|||
window['Browser'] = {
|
||||
init: () => { },
|
||||
};
|
||||
// Emscripten works by expecting the module config to be a global
|
||||
window['Module'] = createEmscriptenModuleInstance(loadAssemblyUrls, resolve, reject);
|
||||
|
||||
addScriptTagsToDocument();
|
||||
// Emscripten works by expecting the module config to be a global
|
||||
// For compatibility with macOS Catalina, we have to assign a temporary value to window.Module
|
||||
// before we start loading the WebAssembly files
|
||||
addGlobalModuleScriptTagsToDocument(() => {
|
||||
window['Module'] = createEmscriptenModuleInstance(loadAssemblyUrls, resolve, reject);
|
||||
addScriptTagsToDocument();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -205,6 +209,23 @@ function addScriptTagsToDocument() {
|
|||
document.body.appendChild(scriptElem);
|
||||
}
|
||||
|
||||
// Due to a strange behavior in macOS Catalina, we have to delay loading the WebAssembly files
|
||||
// until after it finishes evaluating a <script> element that assigns a value to window.Module.
|
||||
// This may be fixed in a later version of macOS/iOS, or even if not it may be possible to reduce
|
||||
// this to a smaller workaround.
|
||||
function addGlobalModuleScriptTagsToDocument(callback: () => void) {
|
||||
const scriptElem = document.createElement('script');
|
||||
|
||||
// This pollutes global but is needed so it can be called from the script.
|
||||
// The callback is put in the global scope so that it can be run after the script is loaded.
|
||||
// onload cannot be used in this case for non-file scripts.
|
||||
window['__wasmmodulecallback__'] = callback;
|
||||
scriptElem.type = 'text/javascript';
|
||||
scriptElem.text = 'var Module; window.__wasmmodulecallback__(); delete window.__wasmmodulecallback__;';
|
||||
|
||||
document.body.appendChild(scriptElem);
|
||||
}
|
||||
|
||||
function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () => void, onError: (reason?: any) => void) {
|
||||
const module = {} as typeof Module;
|
||||
const wasmBinaryFile = '_framework/wasm/mono.wasm';
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalCh
|
|||
import { applyCaptureIdToElement } from './ElementReferenceCapture';
|
||||
import { EventFieldInfo } from './EventFieldInfo';
|
||||
import { dispatchEvent } from './RendererEventDispatcher';
|
||||
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
|
||||
const selectValuePropname = '_blazorSelectValue';
|
||||
const sharedTemplateElemForParsing = document.createElement('template');
|
||||
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
|
||||
const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {};
|
||||
const internalAttributeNamePrefix = '__internal_';
|
||||
const eventPreventDefaultAttributeNamePrefix = 'preventDefault_';
|
||||
const eventStopPropagationAttributeNamePrefix = 'stopPropagation_';
|
||||
|
||||
export class BrowserRenderer {
|
||||
private eventDelegator: EventDelegator;
|
||||
|
|
@ -23,6 +27,11 @@ export class BrowserRenderer {
|
|||
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => {
|
||||
raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs, eventFieldInfo);
|
||||
});
|
||||
|
||||
// We don't yet know whether or not navigation interception will be enabled, but in case it will be,
|
||||
// we wire up the navigation manager to the event delegator so it has the option to participate
|
||||
// in the synthetic event bubbling process later
|
||||
attachNavigationManagerToEventDelegator(this.eventDelegator);
|
||||
}
|
||||
|
||||
public attachRootComponentToLogicalElement(componentId: number, element: LogicalElement): void {
|
||||
|
|
@ -281,15 +290,10 @@ export class BrowserRenderer {
|
|||
private applyAttribute(batch: RenderBatch, componentId: number, toDomElement: Element, attributeFrame: RenderTreeFrame) {
|
||||
const frameReader = batch.frameReader;
|
||||
const attributeName = frameReader.attributeName(attributeFrame)!;
|
||||
const browserRendererId = this.browserRendererId;
|
||||
const eventHandlerId = frameReader.attributeEventHandlerId(attributeFrame);
|
||||
|
||||
if (eventHandlerId) {
|
||||
const firstTwoChars = attributeName.substring(0, 2);
|
||||
const eventName = attributeName.substring(2);
|
||||
if (firstTwoChars !== 'on' || !eventName) {
|
||||
throw new Error(`Attribute has nonzero event handler ID, but attribute name '${attributeName}' does not start with 'on'.`);
|
||||
}
|
||||
const eventName = stripOnPrefix(attributeName);
|
||||
this.eventDelegator.setListener(toDomElement, eventName, eventHandlerId, componentId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -310,8 +314,30 @@ export class BrowserRenderer {
|
|||
return this.tryApplyValueProperty(batch, element, attributeFrame);
|
||||
case 'checked':
|
||||
return this.tryApplyCheckedProperty(batch, element, attributeFrame);
|
||||
default:
|
||||
default: {
|
||||
if (attributeName.startsWith(internalAttributeNamePrefix)) {
|
||||
this.applyInternalAttribute(batch, element, attributeName.substring(internalAttributeNamePrefix.length), attributeFrame);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyInternalAttribute(batch: RenderBatch, element: Element, internalAttributeName: string, attributeFrame: RenderTreeFrame | null) {
|
||||
const attributeValue = attributeFrame ? batch.frameReader.attributeValue(attributeFrame) : null;
|
||||
|
||||
if (internalAttributeName.startsWith(eventStopPropagationAttributeNamePrefix)) {
|
||||
// Stop propagation
|
||||
const eventName = stripOnPrefix(internalAttributeName.substring(eventStopPropagationAttributeNamePrefix.length));
|
||||
this.eventDelegator.setStopPropagation(element, eventName, attributeValue !== null);
|
||||
} else if (internalAttributeName.startsWith(eventPreventDefaultAttributeNamePrefix)) {
|
||||
// Prevent default
|
||||
const eventName = stripOnPrefix(internalAttributeName.substring(eventPreventDefaultAttributeNamePrefix.length));
|
||||
this.eventDelegator.setPreventDefault(element, eventName, attributeValue !== null);
|
||||
} else {
|
||||
// The prefix makes this attribute name reserved, so any other usage is disallowed
|
||||
throw new Error(`Unsupported internal attribute '${internalAttributeName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -477,3 +503,11 @@ function clearBetween(start: Node, end: Node): void {
|
|||
// as it adds noise to the DOM.
|
||||
start.textContent = '!';
|
||||
}
|
||||
|
||||
function stripOnPrefix(attributeName: string) {
|
||||
if (attributeName.startsWith('on')) {
|
||||
return attributeName.substring(2);
|
||||
}
|
||||
|
||||
throw new Error(`Attribute should be an event name, but doesn't start with 'on'. Value: '${attributeName}'`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ export interface OnEventCallback {
|
|||
export class EventDelegator {
|
||||
private static nextEventDelegatorId = 0;
|
||||
|
||||
private eventsCollectionKey: string;
|
||||
private readonly eventsCollectionKey: string;
|
||||
|
||||
private readonly afterClickCallbacks: ((event: MouseEvent) => void)[] = [];
|
||||
|
||||
private eventInfoStore: EventInfoStore;
|
||||
|
||||
|
|
@ -42,21 +44,18 @@ export class EventDelegator {
|
|||
}
|
||||
|
||||
public setListener(element: Element, eventName: string, eventHandlerId: number, renderingComponentId: number) {
|
||||
// Ensure we have a place to store event info for this element
|
||||
let infoForElement: EventHandlerInfosForElement = element[this.eventsCollectionKey];
|
||||
if (!infoForElement) {
|
||||
infoForElement = element[this.eventsCollectionKey] = {};
|
||||
}
|
||||
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
|
||||
const existingHandler = infoForElement.getHandler(eventName);
|
||||
|
||||
if (infoForElement.hasOwnProperty(eventName)) {
|
||||
if (existingHandler) {
|
||||
// We can cheaply update the info on the existing object and don't need any other housekeeping
|
||||
const oldInfo = infoForElement[eventName];
|
||||
this.eventInfoStore.update(oldInfo.eventHandlerId, eventHandlerId);
|
||||
// Note that this also takes care of updating the eventHandlerId on the existing handler object
|
||||
this.eventInfoStore.update(existingHandler.eventHandlerId, eventHandlerId);
|
||||
} else {
|
||||
// Go through the whole flow which might involve registering a new global handler
|
||||
const newInfo = { element, eventName, eventHandlerId, renderingComponentId };
|
||||
this.eventInfoStore.add(newInfo);
|
||||
infoForElement[eventName] = newInfo;
|
||||
infoForElement.setHandler(eventName, newInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,16 +68,31 @@ export class EventDelegator {
|
|||
// Looks like this event handler wasn't already disposed
|
||||
// Remove the associated data from the DOM element
|
||||
const element = info.element;
|
||||
if (element.hasOwnProperty(this.eventsCollectionKey)) {
|
||||
const elementEventInfos: EventHandlerInfosForElement = element[this.eventsCollectionKey];
|
||||
delete elementEventInfos[info.eventName];
|
||||
if (Object.getOwnPropertyNames(elementEventInfos).length === 0) {
|
||||
delete element[this.eventsCollectionKey];
|
||||
}
|
||||
const elementEventInfos = this.getEventHandlerInfosForElement(element, false);
|
||||
if (elementEventInfos) {
|
||||
elementEventInfos.removeHandler(info.eventName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public notifyAfterClick(callback: (event: MouseEvent) => void) {
|
||||
// This is extremely special-case. It's needed so that navigation link click interception
|
||||
// can be sure to run *after* our synthetic bubbling process. If a need arises, we can
|
||||
// generalise this, but right now it's a purely internal detail.
|
||||
this.afterClickCallbacks.push(callback);
|
||||
this.eventInfoStore.addGlobalListener('click'); // Ensure we always listen for this
|
||||
}
|
||||
|
||||
public setStopPropagation(element: Element, eventName: string, value: boolean) {
|
||||
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
|
||||
infoForElement.stopPropagation(eventName, value);
|
||||
}
|
||||
|
||||
public setPreventDefault(element: Element, eventName: string, value: boolean) {
|
||||
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
|
||||
infoForElement.preventDefault(eventName, value);
|
||||
}
|
||||
|
||||
private onGlobalEvent(evt: Event) {
|
||||
if (!(evt.target instanceof Element)) {
|
||||
return;
|
||||
|
|
@ -88,22 +102,46 @@ export class EventDelegator {
|
|||
let candidateElement = evt.target as Element | null;
|
||||
let eventArgs: EventForDotNet<UIEventArgs> | null = null; // Populate lazily
|
||||
const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
|
||||
let stopPropagationWasRequested = false;
|
||||
while (candidateElement) {
|
||||
if (candidateElement.hasOwnProperty(this.eventsCollectionKey)) {
|
||||
const handlerInfos: EventHandlerInfosForElement = candidateElement[this.eventsCollectionKey];
|
||||
if (handlerInfos.hasOwnProperty(evt.type)) {
|
||||
const handlerInfos = this.getEventHandlerInfosForElement(candidateElement, false);
|
||||
if (handlerInfos) {
|
||||
const handlerInfo = handlerInfos.getHandler(evt.type);
|
||||
if (handlerInfo) {
|
||||
// We are going to raise an event for this element, so prepare info needed by the .NET code
|
||||
if (!eventArgs) {
|
||||
eventArgs = EventForDotNet.fromDOMEvent(evt);
|
||||
}
|
||||
|
||||
const handlerInfo = handlerInfos[evt.type];
|
||||
const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
|
||||
this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs, eventFieldInfo);
|
||||
}
|
||||
|
||||
if (handlerInfos.stopPropagation(evt.type)) {
|
||||
stopPropagationWasRequested = true;
|
||||
}
|
||||
|
||||
if (handlerInfos.preventDefault(evt.type)) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
candidateElement = eventIsNonBubbling ? null : candidateElement.parentElement;
|
||||
candidateElement = (eventIsNonBubbling || stopPropagationWasRequested) ? null : candidateElement.parentElement;
|
||||
}
|
||||
|
||||
// Special case for navigation interception
|
||||
if (evt.type === 'click') {
|
||||
this.afterClickCallbacks.forEach(callback => callback(evt as MouseEvent));
|
||||
}
|
||||
}
|
||||
|
||||
private getEventHandlerInfosForElement(element: Element, createIfNeeded: boolean): EventHandlerInfosForElement | null {
|
||||
if (element.hasOwnProperty(this.eventsCollectionKey)) {
|
||||
return element[this.eventsCollectionKey];
|
||||
} else if (createIfNeeded) {
|
||||
return (element[this.eventsCollectionKey] = new EventHandlerInfosForElement());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +164,10 @@ class EventInfoStore {
|
|||
|
||||
this.infosByEventHandlerId[info.eventHandlerId] = info;
|
||||
|
||||
const eventName = info.eventName;
|
||||
this.addGlobalListener(info.eventName);
|
||||
}
|
||||
|
||||
public addGlobalListener(eventName: string) {
|
||||
if (this.countByEventName.hasOwnProperty(eventName)) {
|
||||
this.countByEventName[eventName]++;
|
||||
} else {
|
||||
|
|
@ -168,14 +209,46 @@ class EventInfoStore {
|
|||
}
|
||||
}
|
||||
|
||||
interface EventHandlerInfosForElement {
|
||||
class EventHandlerInfosForElement {
|
||||
// Although we *could* track multiple event handlers per (element, eventName) pair
|
||||
// (since they have distinct eventHandlerId values), there's no point doing so because
|
||||
// our programming model is that you declare event handlers as attributes. An element
|
||||
// can only have one attribute with a given name, hence only one event handler with
|
||||
// that name at any one time.
|
||||
// So to keep things simple, only track one EventHandlerInfo per (element, eventName)
|
||||
[eventName: string]: EventHandlerInfo;
|
||||
private handlers: { [eventName: string]: EventHandlerInfo } = {};
|
||||
private preventDefaultFlags: { [eventName: string]: boolean } | null = null;
|
||||
private stopPropagationFlags: { [eventName: string]: boolean } | null = null;
|
||||
|
||||
public getHandler(eventName: string): EventHandlerInfo | null {
|
||||
return this.handlers.hasOwnProperty(eventName) ? this.handlers[eventName] : null;
|
||||
}
|
||||
|
||||
public setHandler(eventName: string, handler: EventHandlerInfo) {
|
||||
this.handlers[eventName] = handler;
|
||||
}
|
||||
|
||||
public removeHandler(eventName: string) {
|
||||
delete this.handlers[eventName];
|
||||
}
|
||||
|
||||
public preventDefault(eventName: string, setValue?: boolean): boolean {
|
||||
if (setValue !== undefined) {
|
||||
this.preventDefaultFlags = this.preventDefaultFlags || {};
|
||||
this.preventDefaultFlags[eventName] = setValue;
|
||||
}
|
||||
|
||||
return this.preventDefaultFlags ? this.preventDefaultFlags[eventName] : false;
|
||||
}
|
||||
|
||||
public stopPropagation(eventName: string, setValue?: boolean): boolean {
|
||||
if (setValue !== undefined) {
|
||||
this.stopPropagationFlags = this.stopPropagationFlags || {};
|
||||
this.stopPropagationFlags[eventName] = setValue;
|
||||
}
|
||||
|
||||
return this.stopPropagationFlags ? this.stopPropagationFlags[eventName] : false;
|
||||
}
|
||||
}
|
||||
|
||||
interface EventHandlerInfo {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import '@dotnet/jsinterop';
|
||||
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
|
||||
import { EventDelegator } from '../Rendering/EventDelegator';
|
||||
|
||||
let hasRegisteredNavigationInterception = false;
|
||||
let hasEnabledNavigationInterception = false;
|
||||
let hasRegisteredNavigationEventListeners = false;
|
||||
|
||||
// Will be initialized once someone registers
|
||||
|
|
@ -28,21 +29,30 @@ function listenForNavigationEvents(callback: (uri: string, intercepted: boolean)
|
|||
}
|
||||
|
||||
function enableNavigationInterception() {
|
||||
if (hasRegisteredNavigationInterception) {
|
||||
return;
|
||||
}
|
||||
hasEnabledNavigationInterception = true;
|
||||
}
|
||||
|
||||
hasRegisteredNavigationInterception = true;
|
||||
export function attachToEventDelegator(eventDelegator: EventDelegator) {
|
||||
// We need to respond to clicks on <a> elements *after* the EventDelegator has finished
|
||||
// running its simulated bubbling process so that we can respect any preventDefault requests.
|
||||
// So instead of registering our own native event, register using the EventDelegator.
|
||||
eventDelegator.notifyAfterClick(event => {
|
||||
if (!hasEnabledNavigationInterception) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
if (event.button !== 0 || eventHasSpecialKey(event)) {
|
||||
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept clicks on all <a> elements where the href is within the <base href> URI space
|
||||
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
|
||||
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement;
|
||||
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement | null;
|
||||
const hrefAttributeName = 'href';
|
||||
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
|
||||
const targetAttributeValue = anchorTarget.getAttribute('target');
|
||||
|
|
|
|||
|
|
@ -409,6 +409,11 @@ namespace Microsoft.AspNetCore.Components.Web
|
|||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Web.TouchEventArgs> Create(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Func<Microsoft.AspNetCore.Components.Web.TouchEventArgs, System.Threading.Tasks.Task> callback) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Web.WheelEventArgs> Create(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Func<Microsoft.AspNetCore.Components.Web.WheelEventArgs, System.Threading.Tasks.Task> callback) { throw null; }
|
||||
}
|
||||
public static partial class WebRenderTreeBuilderExtensions
|
||||
{
|
||||
public static void AddEventPreventDefaultAttribute(this Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int sequence, string eventName, bool value) { }
|
||||
public static void AddEventStopPropagationAttribute(this Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int sequence, string eventName, bool value) { }
|
||||
}
|
||||
public partial class WheelEventArgs : Microsoft.AspNetCore.Components.Web.MouseEventArgs
|
||||
{
|
||||
public WheelEventArgs() { }
|
||||
|
|
|
|||
|
|
@ -409,6 +409,11 @@ namespace Microsoft.AspNetCore.Components.Web
|
|||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Web.TouchEventArgs> Create(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Func<Microsoft.AspNetCore.Components.Web.TouchEventArgs, System.Threading.Tasks.Task> callback) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Web.WheelEventArgs> Create(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Func<Microsoft.AspNetCore.Components.Web.WheelEventArgs, System.Threading.Tasks.Task> callback) { throw null; }
|
||||
}
|
||||
public static partial class WebRenderTreeBuilderExtensions
|
||||
{
|
||||
public static void AddEventPreventDefaultAttribute(this Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int sequence, string eventName, bool value) { }
|
||||
public static void AddEventStopPropagationAttribute(this Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int sequence, string eventName, bool value) { }
|
||||
}
|
||||
public partial class WheelEventArgs : Microsoft.AspNetCore.Components.Web.MouseEventArgs
|
||||
{
|
||||
public WheelEventArgs() { }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
// 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.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for building a collection of <see cref="RenderTreeFrame"/> entries.
|
||||
/// </summary>
|
||||
public static class WebRenderTreeBuilderExtensions
|
||||
{
|
||||
// The "prevent default" and "stop propagation" flags behave like attributes, in that:
|
||||
// - you can have multiple of them on a given element (for separate events)
|
||||
// - you can add and remove them dynamically
|
||||
// - they are independent of other attributes (e.g., you can "stop propagation" of a given
|
||||
// event type on an element that doesn't itself have a handler for that event)
|
||||
// As such, they are represented as attributes to give the right diffing behavior.
|
||||
//
|
||||
// As a private implementation detail, their internal representation is magic-named
|
||||
// attributes. This may change in the future. If we add support for multiple-same
|
||||
// -named-attributes-per-element (#14365), then we will probably also declare a new
|
||||
// AttributeType concept, and have specific attribute types for these flags, and
|
||||
// the "name" can simply be the name of the event being modified.
|
||||
|
||||
/// <summary>
|
||||
/// Appends a frame representing an instruction to prevent the default action
|
||||
/// for a specified event.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="RenderTreeBuilder"/>.</param>
|
||||
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
|
||||
/// <param name="eventName">The name of the event to be affected.</param>
|
||||
/// <param name="value">True if the default action is to be prevented, otherwise false.</param>
|
||||
public static void AddEventPreventDefaultAttribute(this RenderTreeBuilder builder, int sequence, string eventName, bool value)
|
||||
{
|
||||
builder.AddAttribute(sequence, $"__internal_preventDefault_{eventName}", value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a frame representing an instruction to stop the specified event from
|
||||
/// propagating beyond the current element.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="RenderTreeBuilder"/>.</param>
|
||||
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
|
||||
/// <param name="eventName">The name of the event to be affected.</param>
|
||||
/// <param name="value">True if propagation should be stopped here, otherwise false.</param>
|
||||
public static void AddEventStopPropagationAttribute(this RenderTreeBuilder builder, int sequence, string eventName, bool value)
|
||||
{
|
||||
builder.AddAttribute(sequence, $"__internal_stopPropagation_{eventName}", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@
|
|||
<ItemGroup>
|
||||
<!-- Shared descriptor infrastructure with MVC -->
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -68,9 +68,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
|
||||
var greets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
|
||||
|
||||
Assert.Equal(4, greets.Length); // 1 statically rendered + 3 prerendered
|
||||
Assert.Equal(5, greets.Length); // 1 statically rendered + 3 prerendered + 1 server prerendered
|
||||
Assert.Single(greets, "Hello John");
|
||||
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 prerendered
|
||||
Assert.Single(greets, "Hello Abraham");
|
||||
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 server prerendered without parameters
|
||||
var content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
|
||||
var markers = ReadMarkers(content);
|
||||
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
|
||||
|
|
@ -84,6 +85,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
};
|
||||
Assert.Equal(expectedComponentSequence, componentSequence);
|
||||
|
||||
|
|
@ -93,6 +96,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Browser.Exists(By.CssSelector("h3.interactive"));
|
||||
var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
|
||||
Assert.Equal(7, updatedGreets.Where(g => string.Equals("Hello Alfred", g)).Count());
|
||||
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Albert", g)));
|
||||
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Abraham", g)));
|
||||
}
|
||||
|
||||
private (ServerComponentMarker, ServerComponentMarker)[] ReadMarkers(string content)
|
||||
|
|
|
|||
|
|
@ -99,12 +99,102 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Empty(GetLogLines);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("target")]
|
||||
[InlineData("intermediate")]
|
||||
public void StopPropagation(string whereToStopPropagation)
|
||||
{
|
||||
// If stopPropagation is off, we observe the event on the listener and all its ancestors
|
||||
Browser.FindElement(By.Id("button-with-onclick")).Click();
|
||||
Browser.Equal(new[] { "target onclick", "parent onclick" }, GetLogLines);
|
||||
|
||||
// If stopPropagation is on, the event doesn't reach the ancestor
|
||||
// Note that in the "intermediate element" case, the intermediate element does *not* itself
|
||||
// listen for this event, which shows that stopPropagation works independently of handling
|
||||
ClearLog();
|
||||
Browser.FindElement(By.Id($"{whereToStopPropagation}-stop-propagation")).Click();
|
||||
Browser.FindElement(By.Id("button-with-onclick")).Click();
|
||||
Browser.Equal(new[] { "target onclick" }, GetLogLines);
|
||||
|
||||
// We can also toggle it back off
|
||||
ClearLog();
|
||||
Browser.FindElement(By.Id($"{whereToStopPropagation}-stop-propagation")).Click();
|
||||
Browser.FindElement(By.Id("button-with-onclick")).Click();
|
||||
Browser.Equal(new[] { "target onclick", "parent onclick" }, GetLogLines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventDefaultWorksOnTarget()
|
||||
{
|
||||
// Clicking a checkbox without preventDefault produces both "click" and "change"
|
||||
// events, and it becomes checked
|
||||
var checkboxWithoutPreventDefault = Browser.FindElement(By.Id("checkbox-with-preventDefault-false"));
|
||||
checkboxWithoutPreventDefault.Click();
|
||||
Browser.Equal(new[] { "Checkbox click", "Checkbox change" }, GetLogLines);
|
||||
Browser.True(() => checkboxWithoutPreventDefault.Selected);
|
||||
|
||||
// Clicking a checkbox with preventDefault produces a "click" event, but no "change"
|
||||
// event, and it remains unchecked
|
||||
ClearLog();
|
||||
var checkboxWithPreventDefault = Browser.FindElement(By.Id("checkbox-with-preventDefault-true"));
|
||||
checkboxWithPreventDefault.Click();
|
||||
Browser.Equal(new[] { "Checkbox click" }, GetLogLines);
|
||||
Browser.False(() => checkboxWithPreventDefault.Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventDefault_WorksOnAncestorElement()
|
||||
{
|
||||
// Even though the checkbox we're clicking this case does *not* have preventDefault,
|
||||
// if its ancestor does, then we don't get the "change" event and it remains unchecked
|
||||
Browser.FindElement(By.Id($"ancestor-prevent-default")).Click();
|
||||
var checkboxWithoutPreventDefault = Browser.FindElement(By.Id("checkbox-with-preventDefault-false"));
|
||||
checkboxWithoutPreventDefault.Click();
|
||||
Browser.Equal(new[] { "Checkbox click" }, GetLogLines);
|
||||
Browser.False(() => checkboxWithoutPreventDefault.Selected);
|
||||
|
||||
// We can also toggle it back off dynamically
|
||||
Browser.FindElement(By.Id($"ancestor-prevent-default")).Click();
|
||||
ClearLog();
|
||||
checkboxWithoutPreventDefault.Click();
|
||||
Browser.Equal(new[] { "Checkbox click", "Checkbox change" }, GetLogLines);
|
||||
Browser.True(() => checkboxWithoutPreventDefault.Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreventDefaultCanBlockKeystrokes()
|
||||
{
|
||||
// By default, the textbox accepts keystrokes
|
||||
var textbox = Browser.FindElement(By.Id($"textbox-that-can-block-keystrokes"));
|
||||
textbox.SendKeys("a");
|
||||
Browser.Equal(new[] { "Received keydown" }, GetLogLines);
|
||||
Browser.Equal("a", () => textbox.GetAttribute("value"));
|
||||
|
||||
// We can turn on preventDefault to stop keystrokes
|
||||
// There will still be a keydown event, but we're preventing it from actually changing the textbox value
|
||||
ClearLog();
|
||||
Browser.FindElement(By.Id($"prevent-keydown")).Click();
|
||||
textbox.SendKeys("b");
|
||||
Browser.Equal(new[] { "Received keydown" }, GetLogLines);
|
||||
Browser.Equal("a", () => textbox.GetAttribute("value"));
|
||||
|
||||
// We can turn it back off
|
||||
ClearLog();
|
||||
Browser.FindElement(By.Id($"prevent-keydown")).Click();
|
||||
textbox.SendKeys("c");
|
||||
Browser.Equal(new[] { "Received keydown" }, GetLogLines);
|
||||
Browser.Equal("ac", () => textbox.GetAttribute("value"));
|
||||
}
|
||||
|
||||
private string[] GetLogLines()
|
||||
=> Browser.FindElement(By.TagName("textarea"))
|
||||
.GetAttribute("value")
|
||||
.Replace("\r\n", "\n")
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
void ClearLog()
|
||||
=> Browser.FindElement(By.Id("clear-log")).Click();
|
||||
|
||||
private void TriggerCustomBubblingEvent(string elementId, string eventName)
|
||||
{
|
||||
var jsExecutor = (IJavaScriptExecutor)Browser;
|
||||
|
|
|
|||
|
|
@ -455,6 +455,49 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Equal(0, () => BrowserScrollY);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("external", "ancestor")]
|
||||
[InlineData("external", "target")]
|
||||
[InlineData("external", "descendant")]
|
||||
[InlineData("internal", "ancestor")]
|
||||
[InlineData("internal", "target")]
|
||||
[InlineData("internal", "descendant")]
|
||||
public void PreventDefault_CanBlockNavigation(string navigationType, string whereToPreventDefault)
|
||||
{
|
||||
SetUrlViaPushState("/PreventDefaultCases");
|
||||
var app = Browser.MountTestComponent<TestRouter>();
|
||||
var preventDefaultToggle = app.FindElement(By.CssSelector($".prevent-default .{whereToPreventDefault}"));
|
||||
var linkElement = app.FindElement(By.Id($"{navigationType}-navigation"));
|
||||
var counterButton = app.FindElement(By.ClassName("counter-button"));
|
||||
if (whereToPreventDefault == "descendant")
|
||||
{
|
||||
// We're testing clicks on the link's descendant element
|
||||
linkElement = linkElement.FindElement(By.TagName("span"));
|
||||
}
|
||||
|
||||
// If preventDefault is on, then navigation does not occur
|
||||
preventDefaultToggle.Click();
|
||||
linkElement.Click();
|
||||
|
||||
// We check that no navigation ocurred by observing that we can still use the counter
|
||||
counterButton.Click();
|
||||
Browser.Equal("Counter: 1", () => counterButton.Text);
|
||||
|
||||
// Now if we toggle preventDefault back off, then navigation will occur
|
||||
preventDefaultToggle.Click();
|
||||
linkElement.Click();
|
||||
|
||||
if (navigationType == "external")
|
||||
{
|
||||
Browser.Equal("about:blank", () => Browser.Url);
|
||||
}
|
||||
else
|
||||
{
|
||||
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
|
||||
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
|
||||
}
|
||||
}
|
||||
|
||||
private long BrowserScrollY
|
||||
{
|
||||
get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY");
|
||||
|
|
|
|||
|
|
@ -1,15 +1,67 @@
|
|||
<h3 id="event-bubbling">Bubbling standard event</h3>
|
||||
|
||||
@* Temporarily hard-coding the internal names - this will be replaced once the Razor compiler supports @onevent:stopPropagation and @onevent:preventDefault *@
|
||||
<div @onclick="@(() => LogEvent("parent onclick"))">
|
||||
<button id="button-with-onclick" @onclick="@(() => LogEvent("target onclick"))">Button with onclick handler</button>
|
||||
<button id="button-without-onclick" >Button without onclick handler</button>
|
||||
@* This element shows you can stop propagation even without necessarily also handling the event *@
|
||||
<div __internal_stopPropagation_onclick="@intermediateStopPropagation">
|
||||
<button id="button-with-onclick" @onclick="@(() => LogEvent("target onclick"))" __internal_stopPropagation_onclick="@targetStopPropagation">
|
||||
Button with onclick handler
|
||||
</button>
|
||||
<button id="button-without-onclick">
|
||||
Button without onclick handler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Options</legend>
|
||||
<label>
|
||||
<input id="intermediate-stop-propagation" type="checkbox" @bind="intermediateStopPropagation" />
|
||||
Stop propagation on intermediate element
|
||||
</label>
|
||||
<label>
|
||||
<input id="target-stop-propagation" type="checkbox" @bind="targetStopPropagation" />
|
||||
Stop propagation on target element
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<h3>PreventDefault</h3>
|
||||
|
||||
<div __internal_preventDefault_onclick="@ancestorPreventDefault">
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" id="checkbox-with-preventDefault-true" __internal_preventDefault_onclick @onclick="@(() => LogEvent("Checkbox click"))" @onchange="@(() => LogEvent("Checkbox change"))" />
|
||||
Checkbox with onclick preventDefault
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" id="checkbox-with-preventDefault-false" __internal_preventDefault_onclick="@false" @onclick="@(() => LogEvent("Checkbox click"))" @onchange="@(() => LogEvent("Checkbox change"))" />
|
||||
Checkbox with onclick preventDefault = false
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
Textbox that can block keystrokes: <input id="textbox-that-can-block-keystrokes" __internal_preventDefault_onkeydown="@preventOnKeyDown" @onkeydown="@(() => LogEvent("Received keydown"))" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Options</legend>
|
||||
<label>
|
||||
<input id="ancestor-prevent-default" type="checkbox" @bind="ancestorPreventDefault" />
|
||||
Prevent default on ancestor
|
||||
</label>
|
||||
<label>
|
||||
<input id="prevent-keydown" type="checkbox" @bind="preventOnKeyDown" />
|
||||
Block keystrokes
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<h3>Bubbling custom event</h3>
|
||||
|
||||
<div onsneeze="@(new Action(() => LogEvent("parent onsneeze")))">
|
||||
<div id="element-with-onsneeze" onsneeze="@(new Action(() => LogEvent("target onsneeze")))">Element with onsneeze handler</div>
|
||||
<div id="element-without-onsneeze" >Element without onsneeze handler</div>
|
||||
<div id="element-without-onsneeze">Element without onsneeze handler</div>
|
||||
</div>
|
||||
|
||||
<h3>Non-bubbling standard event</h3>
|
||||
|
|
@ -23,8 +75,13 @@
|
|||
<h3>Event log</h3>
|
||||
|
||||
<textarea readonly @bind="logValue"></textarea>
|
||||
<button id="clear-log" @onclick="@(() => { logValue = string.Empty; })">Clear log</button>
|
||||
|
||||
@code {
|
||||
bool intermediateStopPropagation;
|
||||
bool targetStopPropagation;
|
||||
bool ancestorPreventDefault;
|
||||
bool preventOnKeyDown;
|
||||
string logValue = string.Empty;
|
||||
|
||||
void LogEvent(string message)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
if (firstRender && Name == null)
|
||||
{
|
||||
Name = "Alfred";
|
||||
interactive = "interactive";
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<li><NavLink href="/subdir/WithParameters/Name/Abc/LastName/McDef">With more parameters</NavLink></li>
|
||||
<li><NavLink href="/subdir/LongPage1">Long page 1</NavLink></li>
|
||||
<li><NavLink href="/subdir/LongPage2">Long page 2</NavLink></li>
|
||||
<li><NavLink href="PreventDefaultCases">preventDefault cases</NavLink></li>
|
||||
<li><NavLink>Null href never matches</NavLink></li>
|
||||
</ul>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
@page "/PreventDefaultCases"
|
||||
|
||||
<h2>Interactions with preventDefault</h2>
|
||||
|
||||
<p>
|
||||
Note that navigation actions are independent of event bubbling. Stopping click event propagation before
|
||||
it reaches an <a> element does <em>not</em> stop navigation from happening. This is by design,
|
||||
because the same is true natively in JavaScript. Navigation only responds to preventDefault.
|
||||
</p>
|
||||
|
||||
<p __internal_preventDefault_onclick="@ancestorPreventDefault">
|
||||
<a id="external-navigation" href="about:blank" __internal_preventDefault_onclick="@targetPreventDefault">
|
||||
External navigation
|
||||
<span __internal_preventDefault_onclick="@descendantPreventDefault">[Descendant element]</span>
|
||||
</a>
|
||||
</p>
|
||||
<p __internal_preventDefault_onclick="@ancestorPreventDefault">
|
||||
<a id="internal-navigation" href="Other" __internal_preventDefault_onclick="@targetPreventDefault">
|
||||
Internal navigation
|
||||
<span __internal_preventDefault_onclick="@descendantPreventDefault">[Descendant element]</span>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<fieldset class="prevent-default">
|
||||
<legend>Prevent default on...</legend>
|
||||
<label><input class="ancestor" type="checkbox" @bind="ancestorPreventDefault" /> Ancestor</label>
|
||||
<label><input class="target" type="checkbox" @bind="targetPreventDefault" /> Target</label>
|
||||
<label><input class="descendant" type="checkbox" @bind="descendantPreventDefault" /> Descendant</label>
|
||||
|
||||
@* So we can assert that navigation didn't happen *@
|
||||
<button class="counter-button" @onclick="@(() => counter++)">Counter: @counter</button>
|
||||
</fieldset>
|
||||
|
||||
@code {
|
||||
bool ancestorPreventDefault;
|
||||
bool targetPreventDefault;
|
||||
bool descendantPreventDefault;
|
||||
int counter;
|
||||
}
|
||||
|
|
@ -21,7 +21,16 @@ namespace TestServer
|
|||
services.AddMvc();
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", _ => { /* Controlled below */ });
|
||||
// It's not enough just to return "Access-Control-Allow-Origin: *", because
|
||||
// browsers don't allow wildcards in conjunction with credentials. So we must
|
||||
// specify explicitly which origin we want to allow.
|
||||
|
||||
options.AddPolicy("AllowAll", policy => policy
|
||||
.SetIsOriginAllowed(host => host.StartsWith("http://localhost:") || host.StartsWith("http://127.0.0.1:"))
|
||||
.AllowAnyHeader()
|
||||
.WithExposedHeaders("MyCustomHeader")
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -33,18 +42,6 @@ namespace TestServer
|
|||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
// It's not enough just to return "Access-Control-Allow-Origin: *", because
|
||||
// browsers don't allow wildcards in conjunction with credentials. So we must
|
||||
// specify explicitly which origin we want to allow.
|
||||
app.UseCors(policy =>
|
||||
{
|
||||
policy.SetIsOriginAllowed(host => host.StartsWith("http://localhost:") || host.StartsWith("http://127.0.0.1:"))
|
||||
.AllowAnyHeader()
|
||||
.WithExposedHeaders("MyCustomHeader")
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
});
|
||||
|
||||
// Mount the server-side Blazor app on /subdir
|
||||
app.Map("/subdir", app =>
|
||||
{
|
||||
|
|
@ -52,12 +49,14 @@ namespace TestServer
|
|||
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@
|
|||
<p>Some content after</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="container">
|
||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" }))
|
||||
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" }))
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.IO;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
|
@ -68,9 +69,7 @@ namespace TestServer
|
|||
</html>");
|
||||
var content = writer.ToString();
|
||||
ctx.Response.ContentLength = content.Length;
|
||||
using var responseWriter = new StreamWriter(ctx.Response.Body);
|
||||
await responseWriter.WriteAsync(content);
|
||||
await responseWriter.FlushAsync();
|
||||
await ctx.Response.WriteAsync(content);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@ namespace Microsoft.Net.Http.Headers
|
|||
}
|
||||
public enum SameSiteMode
|
||||
{
|
||||
Unspecified = -1,
|
||||
None = 0,
|
||||
Lax = 1,
|
||||
Strict = 2,
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ namespace Microsoft.Net.Http.Headers
|
|||
{
|
||||
/// <summary>
|
||||
/// Indicates if the client should include a cookie on "same-site" or "cross-site" requests.
|
||||
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
|
||||
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
|
||||
/// </summary>
|
||||
// This mirrors Microsoft.AspNetCore.Http.SameSiteMode
|
||||
public enum SameSiteMode
|
||||
{
|
||||
/// <summary>No SameSite field will be set, the client should follow its default cookie policy.</summary>
|
||||
Unspecified = -1,
|
||||
/// <summary>Indicates the client should disable same-site restrictions.</summary>
|
||||
None = 0,
|
||||
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
|
||||
Lax,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,14 @@ namespace Microsoft.Net.Http.Headers
|
|||
private const string SecureToken = "secure";
|
||||
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
|
||||
private const string SameSiteToken = "samesite";
|
||||
private static readonly string SameSiteNoneToken = SameSiteMode.None.ToString().ToLower();
|
||||
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
|
||||
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
|
||||
|
||||
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
|
||||
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
|
||||
internal static bool SuppressSameSiteNone;
|
||||
|
||||
private const string HttpOnlyToken = "httponly";
|
||||
private const string SeparatorToken = "; ";
|
||||
private const string EqualsToken = "=";
|
||||
|
|
@ -36,6 +42,14 @@ namespace Microsoft.Net.Http.Headers
|
|||
private StringSegment _name;
|
||||
private StringSegment _value;
|
||||
|
||||
static SetCookieHeaderValue()
|
||||
{
|
||||
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
|
||||
{
|
||||
SuppressSameSiteNone = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private SetCookieHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
|
|
@ -92,16 +106,17 @@ namespace Microsoft.Net.Http.Headers
|
|||
|
||||
public bool Secure { get; set; }
|
||||
|
||||
public SameSiteMode SameSite { get; set; }
|
||||
public SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
|
||||
|
||||
public bool HttpOnly { get; set; }
|
||||
|
||||
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
|
||||
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly
|
||||
public override string ToString()
|
||||
{
|
||||
var length = _name.Length + EqualsToken.Length + _value.Length;
|
||||
|
||||
string maxAge = null;
|
||||
string sameSite = null;
|
||||
|
||||
if (Expires.HasValue)
|
||||
{
|
||||
|
|
@ -129,9 +144,20 @@ namespace Microsoft.Net.Http.Headers
|
|||
length += SeparatorToken.Length + SecureToken.Length;
|
||||
}
|
||||
|
||||
if (SameSite != SameSiteMode.None)
|
||||
// Allow for Unspecified (-1) to skip SameSite
|
||||
if (SameSite == SameSiteMode.None && !SuppressSameSiteNone)
|
||||
{
|
||||
var sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
|
||||
sameSite = SameSiteNoneToken;
|
||||
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
|
||||
}
|
||||
else if (SameSite == SameSiteMode.Lax)
|
||||
{
|
||||
sameSite = SameSiteLaxToken;
|
||||
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
|
||||
}
|
||||
else if (SameSite == SameSiteMode.Strict)
|
||||
{
|
||||
sameSite = SameSiteStrictToken;
|
||||
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
|
||||
}
|
||||
|
||||
|
|
@ -140,9 +166,9 @@ namespace Microsoft.Net.Http.Headers
|
|||
length += SeparatorToken.Length + HttpOnlyToken.Length;
|
||||
}
|
||||
|
||||
return string.Create(length, (this, maxAge), (span, tuple) =>
|
||||
return string.Create(length, (this, maxAge, sameSite), (span, tuple) =>
|
||||
{
|
||||
var (headerValue, maxAgeValue) = tuple;
|
||||
var (headerValue, maxAgeValue, sameSite) = tuple;
|
||||
|
||||
Append(ref span, headerValue._name);
|
||||
Append(ref span, EqualsToken);
|
||||
|
|
@ -180,9 +206,9 @@ namespace Microsoft.Net.Http.Headers
|
|||
AppendSegment(ref span, SecureToken, null);
|
||||
}
|
||||
|
||||
if (headerValue.SameSite != SameSiteMode.None)
|
||||
if (sameSite != null)
|
||||
{
|
||||
AppendSegment(ref span, SameSiteToken, headerValue.SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
|
||||
AppendSegment(ref span, SameSiteToken, sameSite);
|
||||
}
|
||||
|
||||
if (headerValue.HttpOnly)
|
||||
|
|
@ -248,9 +274,18 @@ namespace Microsoft.Net.Http.Headers
|
|||
AppendSegment(builder, SecureToken, null);
|
||||
}
|
||||
|
||||
if (SameSite != SameSiteMode.None)
|
||||
// Allow for Unspecified (-1) to skip SameSite
|
||||
if (SameSite == SameSiteMode.None && !SuppressSameSiteNone)
|
||||
{
|
||||
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
|
||||
AppendSegment(builder, SameSiteToken, SameSiteNoneToken);
|
||||
}
|
||||
else if (SameSite == SameSiteMode.Lax)
|
||||
{
|
||||
AppendSegment(builder, SameSiteToken, SameSiteLaxToken);
|
||||
}
|
||||
else if (SameSite == SameSiteMode.Strict)
|
||||
{
|
||||
AppendSegment(builder, SameSiteToken, SameSiteStrictToken);
|
||||
}
|
||||
|
||||
if (HttpOnly)
|
||||
|
|
@ -302,7 +337,7 @@ namespace Microsoft.Net.Http.Headers
|
|||
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
|
||||
}
|
||||
|
||||
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
|
||||
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax|None}; httponly
|
||||
private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
|
@ -437,25 +472,34 @@ namespace Microsoft.Net.Http.Headers
|
|||
{
|
||||
result.Secure = true;
|
||||
}
|
||||
// samesite-av = "SameSite" / "SameSite=" samesite-value
|
||||
// samesite-value = "Strict" / "Lax"
|
||||
// samesite-av = "SameSite=" samesite-value
|
||||
// samesite-value = "Strict" / "Lax" / "None"
|
||||
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!ReadEqualsSign(input, ref offset))
|
||||
{
|
||||
result.SameSite = SameSiteMode.Strict;
|
||||
result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.Unspecified;
|
||||
}
|
||||
else
|
||||
{
|
||||
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
|
||||
|
||||
if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
|
||||
if (StringSegment.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.SameSite = SameSiteMode.Strict;
|
||||
}
|
||||
else if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.SameSite = SameSiteMode.Lax;
|
||||
}
|
||||
else if (!SuppressSameSiteNone
|
||||
&& StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.SameSite = SameSiteMode.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.SameSite = SameSiteMode.Strict;
|
||||
result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.Unspecified;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ namespace Microsoft.Net.Http.Headers
|
|||
{
|
||||
SameSite = SameSiteMode.None,
|
||||
};
|
||||
dataset.Add(header7, "name7=value7");
|
||||
dataset.Add(header7, "name7=value7; samesite=none");
|
||||
|
||||
|
||||
return dataset;
|
||||
|
|
@ -155,9 +155,20 @@ namespace Microsoft.Net.Http.Headers
|
|||
{
|
||||
SameSite = SameSiteMode.Strict
|
||||
};
|
||||
var string6a = "name6=value6; samesite";
|
||||
var string6b = "name6=value6; samesite=Strict";
|
||||
var string6c = "name6=value6; samesite=invalid";
|
||||
var string6 = "name6=value6; samesite=Strict";
|
||||
|
||||
var header7 = new SetCookieHeaderValue("name7", "value7")
|
||||
{
|
||||
SameSite = SameSiteMode.None
|
||||
};
|
||||
var string7 = "name7=value7; samesite=None";
|
||||
|
||||
var header8 = new SetCookieHeaderValue("name8", "value8")
|
||||
{
|
||||
SameSite = SameSiteMode.Unspecified
|
||||
};
|
||||
var string8a = "name8=value8; samesite";
|
||||
var string8b = "name8=value8; samesite=invalid";
|
||||
|
||||
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
|
||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
|
||||
|
|
@ -170,9 +181,10 @@ namespace Microsoft.Net.Http.Headers
|
|||
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
|
||||
dataset.Add(new[] { header5 }.ToList(), new[] { string5a });
|
||||
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
|
||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6a });
|
||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6b });
|
||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6c });
|
||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6 });
|
||||
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
|
||||
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
|
||||
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
|
@ -301,6 +313,28 @@ namespace Microsoft.Net.Http.Headers
|
|||
Assert.Equal(expectedValue, input.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieHeaderValue_ToString_SameSiteNoneCompat()
|
||||
{
|
||||
SetCookieHeaderValue.SuppressSameSiteNone = true;
|
||||
|
||||
var input = new SetCookieHeaderValue("name", "value")
|
||||
{
|
||||
SameSite = SameSiteMode.None,
|
||||
};
|
||||
|
||||
Assert.Equal("name=value", input.ToString());
|
||||
|
||||
SetCookieHeaderValue.SuppressSameSiteNone = false;
|
||||
|
||||
var input2 = new SetCookieHeaderValue("name", "value")
|
||||
{
|
||||
SameSite = SameSiteMode.None,
|
||||
};
|
||||
|
||||
Assert.Equal("name=value; samesite=none", input2.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
|
||||
|
|
@ -312,6 +346,32 @@ namespace Microsoft.Net.Http.Headers
|
|||
Assert.Equal(expectedValue, builder.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieHeaderValue_AppendToStringBuilder_SameSiteNoneCompat()
|
||||
{
|
||||
SetCookieHeaderValue.SuppressSameSiteNone = true;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var input = new SetCookieHeaderValue("name", "value")
|
||||
{
|
||||
SameSite = SameSiteMode.None,
|
||||
};
|
||||
|
||||
input.AppendToStringBuilder(builder);
|
||||
Assert.Equal("name=value", builder.ToString());
|
||||
|
||||
SetCookieHeaderValue.SuppressSameSiteNone = false;
|
||||
|
||||
var builder2 = new StringBuilder();
|
||||
var input2 = new SetCookieHeaderValue("name", "value")
|
||||
{
|
||||
SameSite = SameSiteMode.None,
|
||||
};
|
||||
|
||||
input2.AppendToStringBuilder(builder2);
|
||||
Assert.Equal("name=value; samesite=none", builder2.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
||||
|
|
@ -322,6 +382,31 @@ namespace Microsoft.Net.Http.Headers
|
|||
Assert.Equal(expectedValue, header.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieHeaderValue_Parse_AcceptsValidValues_SameSiteNoneCompat()
|
||||
{
|
||||
SetCookieHeaderValue.SuppressSameSiteNone = true;
|
||||
var header = SetCookieHeaderValue.Parse("name=value; samesite=none");
|
||||
|
||||
var cookie = new SetCookieHeaderValue("name", "value")
|
||||
{
|
||||
SameSite = SameSiteMode.Strict,
|
||||
};
|
||||
|
||||
Assert.Equal(cookie, header);
|
||||
Assert.Equal("name=value; samesite=strict", header.ToString());
|
||||
SetCookieHeaderValue.SuppressSameSiteNone = false;
|
||||
|
||||
var header2 = SetCookieHeaderValue.Parse("name=value; samesite=none");
|
||||
|
||||
var cookie2 = new SetCookieHeaderValue("name", "value")
|
||||
{
|
||||
SameSite = SameSiteMode.None,
|
||||
};
|
||||
Assert.Equal(cookie2, header2);
|
||||
Assert.Equal("name=value; samesite=none", header2.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
||||
|
|
@ -332,6 +417,31 @@ namespace Microsoft.Net.Http.Headers
|
|||
Assert.Equal(expectedValue, header.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieHeaderValue_TryParse_AcceptsValidValues_SameSiteNoneCompat()
|
||||
{
|
||||
SetCookieHeaderValue.SuppressSameSiteNone = true;
|
||||
Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header));
|
||||
var cookie = new SetCookieHeaderValue("name", "value")
|
||||
{
|
||||
SameSite = SameSiteMode.Strict,
|
||||
};
|
||||
|
||||
Assert.Equal(cookie, header);
|
||||
Assert.Equal("name=value; samesite=strict", header.ToString());
|
||||
|
||||
SetCookieHeaderValue.SuppressSameSiteNone = false;
|
||||
|
||||
Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header2));
|
||||
var cookie2 = new SetCookieHeaderValue("name", "value")
|
||||
{
|
||||
SameSite = SameSiteMode.None,
|
||||
};
|
||||
|
||||
Assert.Equal(cookie2, header2);
|
||||
Assert.Equal("name=value; samesite=none", header2.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,20 @@ namespace Microsoft.AspNetCore.Http
|
|||
/// </summary>
|
||||
public class CookieBuilder
|
||||
{
|
||||
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
|
||||
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
|
||||
internal static bool SuppressSameSiteNone;
|
||||
|
||||
private string _name;
|
||||
|
||||
static CookieBuilder()
|
||||
{
|
||||
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
|
||||
{
|
||||
SuppressSameSiteNone = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the cookie.
|
||||
/// </summary>
|
||||
|
|
@ -49,12 +61,12 @@ namespace Microsoft.AspNetCore.Http
|
|||
public virtual bool HttpOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.None"/>
|
||||
/// The SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Unspecified"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Determines the value that will set on <seealso cref="CookieOptions.SameSite"/>.
|
||||
/// </remarks>
|
||||
public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.None;
|
||||
public virtual SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
|
||||
|
||||
/// <summary>
|
||||
/// The policy that will be used to determine <seealso cref="CookieOptions.Secure"/>.
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Http
|
|||
}
|
||||
public enum SameSiteMode
|
||||
{
|
||||
Unspecified = -1,
|
||||
None = 0,
|
||||
Lax = 1,
|
||||
Strict = 2,
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Http
|
|||
}
|
||||
public enum SameSiteMode
|
||||
{
|
||||
Unspecified = -1,
|
||||
None = 0,
|
||||
Lax = 1,
|
||||
Strict = 2,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,18 @@ namespace Microsoft.AspNetCore.Http
|
|||
/// </summary>
|
||||
public class CookieOptions
|
||||
{
|
||||
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
|
||||
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
|
||||
internal static bool SuppressSameSiteNone;
|
||||
|
||||
static CookieOptions()
|
||||
{
|
||||
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
|
||||
{
|
||||
SuppressSameSiteNone = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default cookie with a path of '/'.
|
||||
/// </summary>
|
||||
|
|
@ -43,10 +55,10 @@ namespace Microsoft.AspNetCore.Http
|
|||
public bool Secure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value for the SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.None"/>
|
||||
/// Gets or sets the value for the SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Unspecified"/>
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="SameSiteMode"/> representing the enforcement mode of the cookie.</returns>
|
||||
public SameSiteMode SameSite { get; set; } = SameSiteMode.None;
|
||||
public SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script.
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ namespace Microsoft.AspNetCore.Http
|
|||
{
|
||||
/// <summary>
|
||||
/// Used to set the SameSite field on response cookies to indicate if those cookies should be included by the client on future "same-site" or "cross-site" requests.
|
||||
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
|
||||
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
|
||||
/// </summary>
|
||||
// This mirrors Microsoft.Net.Http.Headers.SameSiteMode
|
||||
public enum SameSiteMode
|
||||
{
|
||||
/// <summary>No SameSite field will be set, the client should follow its default cookie policy.</summary>
|
||||
Unspecified = -1,
|
||||
/// <summary>Indicates the client should disable same-site restrictions.</summary>
|
||||
None = 0,
|
||||
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
|
||||
Lax,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebUtilities.Performance", "WebUtilities\perf\Microsoft.AspNetCore.WebUtilities.Performance\Microsoft.AspNetCore.WebUtilities.Performance.csproj", "{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization.Policy", "..\Security\Authorization\Policy\src\Microsoft.AspNetCore.Authorization.Policy.csproj", "{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cors", "..\Middleware\CORS\src\Microsoft.AspNetCore.Cors.csproj", "{09168958-FD5B-4D25-8FBF-75E2C80D903B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -603,6 +607,30 @@ Global
|
|||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -651,6 +679,8 @@ Global
|
|||
{611794D2-EF3A-422A-A077-23E61C7ADE49} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
||||
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA}
|
||||
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
||||
{09168958-FD5B-4D25-8FBF-75E2C80D903B} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {85B5E151-2E9D-419C-83DD-0DDCF446C83A}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -14,8 +13,8 @@ namespace Microsoft.AspNetCore.Routing
|
|||
{
|
||||
internal sealed class EndpointMiddleware
|
||||
{
|
||||
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked";
|
||||
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";
|
||||
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareWithEndpointInvoked";
|
||||
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly RequestDelegate _next;
|
||||
|
|
@ -91,7 +90,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " +
|
||||
"but a middleware was not found that supports authorization." +
|
||||
Environment.NewLine +
|
||||
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.");
|
||||
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).");
|
||||
}
|
||||
|
||||
private static void ThrowMissingCorsMiddlewareException(Endpoint endpoint)
|
||||
|
|
@ -99,7 +98,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " +
|
||||
"but a middleware was not found that supports CORS." +
|
||||
Environment.NewLine +
|
||||
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code.");
|
||||
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).");
|
||||
}
|
||||
|
||||
private static class Log
|
||||
|
|
|
|||
|
|
@ -0,0 +1,240 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.FunctionalTests
|
||||
{
|
||||
public class EndpointRoutingIntegrationTest
|
||||
{
|
||||
private static readonly RequestDelegate TestDelegate = async context => await Task.Yield();
|
||||
private static readonly string AuthErrorMessage = "Endpoint / contains authorization metadata, but a middleware was not found that supports authorization." +
|
||||
Environment.NewLine +
|
||||
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. " +
|
||||
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
|
||||
|
||||
private static readonly string CORSErrorMessage = "Endpoint / contains CORS metadata, but a middleware was not found that supports CORS." +
|
||||
Environment.NewLine +
|
||||
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code. " +
|
||||
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationMiddleware_WhenNoAuthMetadataIsConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate));
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddAuthorization();
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var response = await server.CreateRequest("/").SendAsync("GET");
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationMiddleware_WhenEndpointIsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate));
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddAuthorization();
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var response = await server.CreateRequest("/not-found").SendAsync("GET");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationMiddleware_WithAuthorizedEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var response = await server.CreateRequest("/").SendAsync("GET");
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationMiddleware_NotConfigured_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
|
||||
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.CreateRequest("/").SendAsync("GET"));
|
||||
Assert.Equal(AuthErrorMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationMiddleware_NotConfigured_WhenEndpointIsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var response = await server.CreateRequest("/not-found").SendAsync("GET");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationMiddleware_ConfiguredBeforeRouting_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseAuthorization();
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.CreateRequest("/").SendAsync("GET"));
|
||||
Assert.Equal(AuthErrorMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationMiddleware_ConfiguredAfterRouting_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
|
||||
app.UseAuthorization();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.CreateRequest("/").SendAsync("GET"));
|
||||
Assert.Equal(AuthErrorMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CorsMiddleware_WithCorsEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireCors(policy => policy.AllowAnyOrigin()));
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddCors();
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var response = await server.CreateRequest("/").SendAsync("PUT");
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CorsMiddleware_ConfiguredBeforeRouting_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebHostBuilder();
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireCors(policy => policy.AllowAnyOrigin()));
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddCors();
|
||||
services.AddRouting();
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.CreateRequest("/").SendAsync("GET"));
|
||||
Assert.Equal(CORSErrorMessage, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
|
||||
<Reference Include="Microsoft.AspNetCore.Cors" />
|
||||
<Reference Include="Microsoft.AspNetCore.Routing" />
|
||||
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ namespace Microsoft.AspNetCore.Routing
|
|||
// Arrange
|
||||
var expected = "Endpoint Test contains authorization metadata, but a middleware was not found that supports authorization." +
|
||||
Environment.NewLine +
|
||||
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.";
|
||||
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. " +
|
||||
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
RequestServices = new ServiceProvider()
|
||||
|
|
@ -197,7 +198,8 @@ namespace Microsoft.AspNetCore.Routing
|
|||
// Arrange
|
||||
var expected = "Endpoint Test contains CORS metadata, but a middleware was not found that supports CORS." +
|
||||
Environment.NewLine +
|
||||
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code.";
|
||||
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code. " +
|
||||
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
RequestServices = new ServiceProvider()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"solution": {
|
||||
"path": "D:\\work\\aspnetcore\\src\\Middleware\\Middleware.sln",
|
||||
"projects": [
|
||||
"CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
|
||||
"CORS\\test\\UnitTests\\Microsoft.AspNetCore.Cors.Test.csproj",
|
||||
"CORS\\test\\testassets\\CorsMiddlewareWebSite\\CorsMiddlewareWebSite.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,8 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
public class CorsMiddleware
|
||||
{
|
||||
// Property key is used by other systems, e.g. MVC, to check if CORS middleware has run
|
||||
private const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";
|
||||
private static readonly object CorsMiddlewareInvokedValue = new object();
|
||||
private const string CorsMiddlewareWithEndpointInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
|
||||
private static readonly object CorsMiddlewareWithEndpointInvokedValue = new object();
|
||||
|
||||
private readonly Func<object, Task> OnResponseStartingDelegate = OnResponseStarting;
|
||||
private readonly RequestDelegate _next;
|
||||
|
|
@ -116,14 +116,6 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
/// <inheritdoc />
|
||||
public Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider)
|
||||
{
|
||||
// Flag to indicate to other systems, that CORS middleware was run for this request
|
||||
context.Items[CorsMiddlewareInvokedKey] = CorsMiddlewareInvokedValue;
|
||||
|
||||
if (!context.Request.Headers.ContainsKey(CorsConstants.Origin))
|
||||
{
|
||||
return _next(context);
|
||||
}
|
||||
|
||||
// CORS policy resolution rules:
|
||||
//
|
||||
// 1. If there is an endpoint with IDisableCorsAttribute then CORS is not run
|
||||
|
|
@ -131,9 +123,20 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
// there is an endpoint with IEnableCorsAttribute that has a policy name then
|
||||
// fetch policy by name, prioritizing it above policy on middleware
|
||||
// 3. If there is no policy on middleware then use name on middleware
|
||||
|
||||
var endpoint = context.GetEndpoint();
|
||||
|
||||
if (endpoint != null)
|
||||
{
|
||||
// EndpointRoutingMiddleware uses this flag to check if the CORS middleware processed CORS metadata on the endpoint.
|
||||
// The CORS middleware can only make this claim if it observes an actual endpoint.
|
||||
context.Items[CorsMiddlewareWithEndpointInvokedKey] = CorsMiddlewareWithEndpointInvokedValue;
|
||||
}
|
||||
|
||||
if (!context.Request.Headers.ContainsKey(CorsConstants.Origin))
|
||||
{
|
||||
return _next(context);
|
||||
}
|
||||
|
||||
// Get the most significant CORS metadata for the endpoint
|
||||
// For backwards compatibility reasons this is then downcast to Enable/Disable metadata
|
||||
var corsMetadata = endpoint?.Metadata.GetMetadata<ICorsMetadata>();
|
||||
|
|
|
|||
|
|
@ -918,7 +918,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
await middleware.Invoke(httpContext, mockProvider);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareInvoked"));
|
||||
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -936,12 +936,37 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
"DefaultPolicyName");
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint"));
|
||||
|
||||
// Act
|
||||
await middleware.Invoke(httpContext, mockProvider);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareInvoked"));
|
||||
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_WithoutEndpoint_InvokeFlagSet()
|
||||
{
|
||||
// Arrange
|
||||
var corsService = Mock.Of<ICorsService>();
|
||||
var mockProvider = Mock.Of<ICorsPolicyProvider>();
|
||||
var loggerFactory = NullLoggerFactory.Instance;
|
||||
|
||||
var middleware = new CorsMiddleware(
|
||||
Mock.Of<RequestDelegate>(),
|
||||
corsService,
|
||||
loggerFactory,
|
||||
"DefaultPolicyName");
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
|
||||
|
||||
// Act
|
||||
await middleware.Invoke(httpContext, mockProvider);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Globalization;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
|
|
@ -188,6 +189,12 @@ namespace Microsoft.AspNetCore.Builder
|
|||
OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
|
||||
});
|
||||
|
||||
// An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
|
||||
// the endpoint and route values to ensure things are re-calculated.
|
||||
context.HttpContext.SetEndpoint(endpoint: null);
|
||||
var routeValuesFeature = context.HttpContext.Features.Get<IRouteValuesFeature>();
|
||||
routeValuesFeature?.RouteValues?.Clear();
|
||||
|
||||
context.HttpContext.Request.Path = newPath;
|
||||
context.HttpContext.Request.QueryString = newQueryString;
|
||||
try
|
||||
|
|
|
|||
|
|
@ -177,112 +177,6 @@ namespace Microsoft.AspNetCore.Diagnostics
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Redirect_StatusPage()
|
||||
{
|
||||
var expectedStatusCode = 432;
|
||||
var destination = "/location";
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseStatusCodePagesWithRedirects("/errorPage?id={0}");
|
||||
|
||||
app.Map(destination, (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run((httpContext) =>
|
||||
{
|
||||
httpContext.Response.StatusCode = expectedStatusCode;
|
||||
return Task.FromResult(1);
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/errorPage", (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run(async (httpContext) =>
|
||||
{
|
||||
await httpContext.Response.WriteAsync(httpContext.Request.QueryString.Value);
|
||||
});
|
||||
});
|
||||
|
||||
app.Run((context) =>
|
||||
{
|
||||
|
||||
throw new InvalidOperationException($"Invalid input provided. {context.Request.Path}");
|
||||
});
|
||||
});
|
||||
var expectedQueryString = $"?id={expectedStatusCode}";
|
||||
var expectedUri = $"/errorPage{expectedQueryString}";
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
|
||||
var client = server.CreateClient();
|
||||
var response = await client.GetAsync(destination);
|
||||
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
|
||||
Assert.Equal(expectedUri, response.Headers.First(s => s.Key == "Location").Value.First());
|
||||
|
||||
response = await client.GetAsync(expectedUri);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedQueryString, content);
|
||||
Assert.Equal(expectedQueryString, response.RequestMessage.RequestUri.Query);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reexecute_CanRetrieveInformationAboutOriginalRequest()
|
||||
{
|
||||
var expectedStatusCode = 432;
|
||||
var destination = "/location";
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var beforeNext = context.Request.QueryString;
|
||||
await next();
|
||||
var afterNext = context.Request.QueryString;
|
||||
|
||||
Assert.Equal(beforeNext, afterNext);
|
||||
});
|
||||
app.UseStatusCodePagesWithReExecute(pathFormat: "/errorPage", queryFormat: "?id={0}");
|
||||
|
||||
app.Map(destination, (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run((httpContext) =>
|
||||
{
|
||||
httpContext.Response.StatusCode = expectedStatusCode;
|
||||
return Task.FromResult(1);
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/errorPage", (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run(async (httpContext) =>
|
||||
{
|
||||
var statusCodeReExecuteFeature = httpContext.Features.Get<IStatusCodeReExecuteFeature>();
|
||||
await httpContext.Response.WriteAsync(
|
||||
httpContext.Request.QueryString.Value
|
||||
+ ", "
|
||||
+ statusCodeReExecuteFeature.OriginalPath
|
||||
+ ", "
|
||||
+ statusCodeReExecuteFeature.OriginalQueryString);
|
||||
});
|
||||
});
|
||||
|
||||
app.Run((context) =>
|
||||
{
|
||||
throw new InvalidOperationException("Invalid input provided.");
|
||||
});
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var response = await client.GetAsync(destination + "?name=James");
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal($"?id={expectedStatusCode}, /location, ?name=James", content);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearsCacheHeaders_SetByReexecutionPathHandlers()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
// 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.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Diagnostics
|
||||
{
|
||||
public class StatusCodeMiddlewareTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task Redirect_StatusPage()
|
||||
{
|
||||
var expectedStatusCode = 432;
|
||||
var destination = "/location";
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseStatusCodePagesWithRedirects("/errorPage?id={0}");
|
||||
|
||||
app.Map(destination, (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run((httpContext) =>
|
||||
{
|
||||
httpContext.Response.StatusCode = expectedStatusCode;
|
||||
return Task.FromResult(1);
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/errorPage", (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run(async (httpContext) =>
|
||||
{
|
||||
await httpContext.Response.WriteAsync(httpContext.Request.QueryString.Value);
|
||||
});
|
||||
});
|
||||
|
||||
app.Run((context) =>
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid input provided. {context.Request.Path}");
|
||||
});
|
||||
});
|
||||
var expectedQueryString = $"?id={expectedStatusCode}";
|
||||
var expectedUri = $"/errorPage{expectedQueryString}";
|
||||
using var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
var response = await client.GetAsync(destination);
|
||||
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
|
||||
Assert.Equal(expectedUri, response.Headers.First(s => s.Key == "Location").Value.First());
|
||||
|
||||
response = await client.GetAsync(expectedUri);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedQueryString, content);
|
||||
Assert.Equal(expectedQueryString, response.RequestMessage.RequestUri.Query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reexecute_CanRetrieveInformationAboutOriginalRequest()
|
||||
{
|
||||
var expectedStatusCode = 432;
|
||||
var destination = "/location";
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var beforeNext = context.Request.QueryString;
|
||||
await next();
|
||||
var afterNext = context.Request.QueryString;
|
||||
|
||||
Assert.Equal(beforeNext, afterNext);
|
||||
});
|
||||
app.UseStatusCodePagesWithReExecute(pathFormat: "/errorPage", queryFormat: "?id={0}");
|
||||
|
||||
app.Map(destination, (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run((httpContext) =>
|
||||
{
|
||||
httpContext.Response.StatusCode = expectedStatusCode;
|
||||
return Task.FromResult(1);
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/errorPage", (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run(async (httpContext) =>
|
||||
{
|
||||
var statusCodeReExecuteFeature = httpContext.Features.Get<IStatusCodeReExecuteFeature>();
|
||||
await httpContext.Response.WriteAsync(
|
||||
httpContext.Request.QueryString.Value
|
||||
+ ", "
|
||||
+ statusCodeReExecuteFeature.OriginalPath
|
||||
+ ", "
|
||||
+ statusCodeReExecuteFeature.OriginalQueryString);
|
||||
});
|
||||
});
|
||||
|
||||
app.Run((context) =>
|
||||
{
|
||||
throw new InvalidOperationException("Invalid input provided.");
|
||||
});
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
var response = await client.GetAsync(destination + "?name=James");
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal($"?id={expectedStatusCode}, /location, ?name=James", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reexecute_ClearsEndpointAndRouteData()
|
||||
{
|
||||
var expectedStatusCode = 432;
|
||||
var destination = "/location";
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseStatusCodePagesWithReExecute(pathFormat: "/errorPage", queryFormat: "?id={0}");
|
||||
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
Assert.Empty(context.Request.RouteValues);
|
||||
Assert.Null(context.GetEndpoint());
|
||||
return next();
|
||||
});
|
||||
|
||||
app.Map(destination, (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run((httpContext) =>
|
||||
{
|
||||
httpContext.SetEndpoint(new Endpoint((_) => Task.CompletedTask, new EndpointMetadataCollection(), "Test"));
|
||||
httpContext.Request.RouteValues["John"] = "Doe";
|
||||
httpContext.Response.StatusCode = expectedStatusCode;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/errorPage", (innerAppBuilder) =>
|
||||
{
|
||||
innerAppBuilder.Run(async (httpContext) =>
|
||||
{
|
||||
var statusCodeReExecuteFeature = httpContext.Features.Get<IStatusCodeReExecuteFeature>();
|
||||
await httpContext.Response.WriteAsync(
|
||||
httpContext.Request.QueryString.Value
|
||||
+ ", "
|
||||
+ statusCodeReExecuteFeature.OriginalPath
|
||||
+ ", "
|
||||
+ statusCodeReExecuteFeature.OriginalQueryString);
|
||||
});
|
||||
});
|
||||
|
||||
app.Run((context) =>
|
||||
{
|
||||
throw new InvalidOperationException("Invalid input provided.");
|
||||
});
|
||||
});
|
||||
|
||||
using var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
var response = await client.GetAsync(destination + "?name=James");
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal($"?id={expectedStatusCode}, /location, ?name=James", content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,11 +94,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
|||
|
||||
private static async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
|
||||
{
|
||||
if (parametersCollection.GetEnumerator().MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Prerendering server components with parameters is not supported.");
|
||||
}
|
||||
|
||||
var serviceProvider = context.RequestServices;
|
||||
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
|
||||
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
|
||||
|
|
@ -106,6 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
|||
var currentInvocation = invocationSerializer.SerializeInvocation(
|
||||
invocationId,
|
||||
type,
|
||||
parametersCollection,
|
||||
prerendered: true);
|
||||
|
||||
var result = await prerenderer.PrerenderComponentAsync(
|
||||
|
|
@ -121,14 +117,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
|||
|
||||
private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
|
||||
{
|
||||
if (parametersCollection.GetEnumerator().MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Server components with parameters are not supported.");
|
||||
}
|
||||
|
||||
var serviceProvider = context.RequestServices;
|
||||
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
|
||||
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, prerendered: false);
|
||||
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
|
||||
|
||||
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -19,22 +19,27 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||
.ToTimeLimitedDataProtector();
|
||||
|
||||
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, bool prerendered)
|
||||
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, bool prerendered)
|
||||
{
|
||||
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type);
|
||||
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters);
|
||||
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
|
||||
}
|
||||
|
||||
private (int sequence, string payload) CreateSerializedServerComponent(
|
||||
ServerComponentInvocationSequence invocationId,
|
||||
Type rootComponent)
|
||||
Type rootComponent,
|
||||
ParameterView parameters)
|
||||
{
|
||||
var sequence = invocationId.Next();
|
||||
|
||||
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
|
||||
|
||||
var serverComponent = new ServerComponent(
|
||||
sequence,
|
||||
rootComponent.Assembly.GetName().Name,
|
||||
rootComponent.FullName,
|
||||
definitions,
|
||||
values,
|
||||
invocationId.Value);
|
||||
|
||||
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
|
|
|
|||
|
|
@ -188,25 +188,208 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
Assert.Equal("<p>Hello Steve!</p>", content);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(RenderMode.Server, "Server components with parameters are not supported.")]
|
||||
[InlineData(RenderMode.ServerPrerendered, "Prerendering server components with parameters is not supported.")]
|
||||
public async Task ComponentWithParametersObject_ThrowsInvalidOperationExceptionForServerRenderModes(
|
||||
RenderMode renderMode,
|
||||
string expectedMessage)
|
||||
[Fact]
|
||||
public async Task CanRender_ComponentWithParameters_ServerMode()
|
||||
{
|
||||
// Arrange
|
||||
var helper = CreateHelper();
|
||||
var writer = new StringWriter();
|
||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||
.ToTimeLimitedDataProtector();
|
||||
|
||||
// Act & Assert
|
||||
var result = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<GreetingComponent>(
|
||||
renderMode,
|
||||
// Act
|
||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
||||
RenderMode.Server,
|
||||
new
|
||||
{
|
||||
Name = "Steve"
|
||||
}));
|
||||
Assert.Equal(expectedMessage, result.Message);
|
||||
Name = "Daniel"
|
||||
});
|
||||
result.WriteTo(writer, HtmlEncoder.Default);
|
||||
var content = writer.ToString();
|
||||
var match = Regex.Match(content, ServerComponentPattern);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
var marker = JsonSerializer.Deserialize<ServerComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.Equal(0, marker.Sequence);
|
||||
Assert.Null(marker.PrerenderId);
|
||||
Assert.NotNull(marker.Descriptor);
|
||||
Assert.Equal("server", marker.Type);
|
||||
|
||||
var unprotectedServerComponent = protector.Unprotect(marker.Descriptor);
|
||||
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.Equal(0, serverComponent.Sequence);
|
||||
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
|
||||
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
|
||||
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
|
||||
|
||||
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
|
||||
Assert.Equal("Name", parameterDefinition.Name);
|
||||
Assert.Equal("System.String", parameterDefinition.TypeName);
|
||||
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
|
||||
|
||||
var value = Assert.Single(serverComponent.ParameterValues);
|
||||
var rawValue = Assert.IsType<JsonElement>(value);
|
||||
Assert.Equal("Daniel", rawValue.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRender_ComponentWithNullParameters_ServerMode()
|
||||
{
|
||||
// Arrange
|
||||
var helper = CreateHelper();
|
||||
var writer = new StringWriter();
|
||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||
.ToTimeLimitedDataProtector();
|
||||
|
||||
// Act
|
||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
||||
RenderMode.Server,
|
||||
new
|
||||
{
|
||||
Name = (string)null
|
||||
});
|
||||
result.WriteTo(writer, HtmlEncoder.Default);
|
||||
var content = writer.ToString();
|
||||
var match = Regex.Match(content, ServerComponentPattern);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
var marker = JsonSerializer.Deserialize<ServerComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.Equal(0, marker.Sequence);
|
||||
Assert.Null(marker.PrerenderId);
|
||||
Assert.NotNull(marker.Descriptor);
|
||||
Assert.Equal("server", marker.Type);
|
||||
|
||||
var unprotectedServerComponent = protector.Unprotect(marker.Descriptor);
|
||||
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.Equal(0, serverComponent.Sequence);
|
||||
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
|
||||
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
|
||||
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
|
||||
|
||||
Assert.NotNull(serverComponent.ParameterDefinitions);
|
||||
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
|
||||
Assert.Equal("Name", parameterDefinition.Name);
|
||||
Assert.Null(parameterDefinition.TypeName);
|
||||
Assert.Null(parameterDefinition.Assembly);
|
||||
|
||||
var value = Assert.Single(serverComponent.ParameterValues);;
|
||||
Assert.Null(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanPrerender_ComponentWithParameters_ServerMode()
|
||||
{
|
||||
// Arrange
|
||||
var helper = CreateHelper();
|
||||
var writer = new StringWriter();
|
||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||
.ToTimeLimitedDataProtector();
|
||||
|
||||
// Act
|
||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
||||
RenderMode.ServerPrerendered,
|
||||
new
|
||||
{
|
||||
Name = "Daniel"
|
||||
});
|
||||
result.WriteTo(writer, HtmlEncoder.Default);
|
||||
var content = writer.ToString();
|
||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
var preamble = match.Groups["preamble"].Value;
|
||||
var preambleMarker = JsonSerializer.Deserialize<ServerComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.Equal(0, preambleMarker.Sequence);
|
||||
Assert.NotNull(preambleMarker.PrerenderId);
|
||||
Assert.NotNull(preambleMarker.Descriptor);
|
||||
Assert.Equal("server", preambleMarker.Type);
|
||||
|
||||
var unprotectedServerComponent = protector.Unprotect(preambleMarker.Descriptor);
|
||||
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.NotEqual(default, serverComponent);
|
||||
Assert.Equal(0, serverComponent.Sequence);
|
||||
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
|
||||
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
|
||||
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
|
||||
|
||||
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
|
||||
Assert.Equal("Name", parameterDefinition.Name);
|
||||
Assert.Equal("System.String", parameterDefinition.TypeName);
|
||||
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
|
||||
|
||||
var value = Assert.Single(serverComponent.ParameterValues);
|
||||
var rawValue = Assert.IsType<JsonElement>(value);
|
||||
Assert.Equal("Daniel", rawValue.GetString());
|
||||
|
||||
var prerenderedContent = match.Groups["content"].Value;
|
||||
Assert.Equal("<p>Hello Daniel!</p>", prerenderedContent);
|
||||
|
||||
var epilogue = match.Groups["epilogue"].Value;
|
||||
var epilogueMarker = JsonSerializer.Deserialize<ServerComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
|
||||
Assert.Null(epilogueMarker.Sequence);
|
||||
Assert.Null(epilogueMarker.Descriptor);
|
||||
Assert.Null(epilogueMarker.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanPrerender_ComponentWithNullParameters_ServerMode()
|
||||
{
|
||||
// Arrange
|
||||
var helper = CreateHelper();
|
||||
var writer = new StringWriter();
|
||||
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||
.ToTimeLimitedDataProtector();
|
||||
|
||||
// Act
|
||||
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
||||
RenderMode.ServerPrerendered,
|
||||
new
|
||||
{
|
||||
Name = (string)null
|
||||
});
|
||||
result.WriteTo(writer, HtmlEncoder.Default);
|
||||
var content = writer.ToString();
|
||||
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
|
||||
|
||||
// Assert
|
||||
Assert.True(match.Success);
|
||||
var preamble = match.Groups["preamble"].Value;
|
||||
var preambleMarker = JsonSerializer.Deserialize<ServerComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.Equal(0, preambleMarker.Sequence);
|
||||
Assert.NotNull(preambleMarker.PrerenderId);
|
||||
Assert.NotNull(preambleMarker.Descriptor);
|
||||
Assert.Equal("server", preambleMarker.Type);
|
||||
|
||||
var unprotectedServerComponent = protector.Unprotect(preambleMarker.Descriptor);
|
||||
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.NotEqual(default, serverComponent);
|
||||
Assert.Equal(0, serverComponent.Sequence);
|
||||
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
|
||||
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
|
||||
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
|
||||
|
||||
Assert.NotNull(serverComponent.ParameterDefinitions);
|
||||
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
|
||||
Assert.Equal("Name", parameterDefinition.Name);
|
||||
Assert.Null(parameterDefinition.TypeName);
|
||||
Assert.Null(parameterDefinition.Assembly);
|
||||
|
||||
var value = Assert.Single(serverComponent.ParameterValues);
|
||||
Assert.Null(value);
|
||||
|
||||
var prerenderedContent = match.Groups["content"].Value;
|
||||
Assert.Equal("<p>Hello (null)!</p>", prerenderedContent);
|
||||
|
||||
var epilogue = match.Groups["epilogue"].Value;
|
||||
var epilogueMarker = JsonSerializer.Deserialize<ServerComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
|
||||
Assert.Null(epilogueMarker.Sequence);
|
||||
Assert.Null(epilogueMarker.Descriptor);
|
||||
Assert.Null(epilogueMarker.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -547,7 +730,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
var s = 0;
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(s++, "p");
|
||||
builder.AddContent(s++, $"Hello {Name}!");
|
||||
builder.AddContent(s++, $"Hello {Name ?? ("(null)")}!");
|
||||
builder.CloseElement();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorComponents
|
|||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void RenderComponentAsync_MarksSelectedOptionsAsSelected()
|
||||
{
|
||||
|
|
@ -668,18 +668,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorComponents
|
|||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
_renderHandle.Render(CreateRenderFragment(parameters));
|
||||
var content = parameters.GetValueOrDefault<string>("Value");
|
||||
_renderHandle.Render(CreateRenderFragment(content));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private RenderFragment CreateRenderFragment(ParameterView parameters)
|
||||
private RenderFragment CreateRenderFragment(string content)
|
||||
{
|
||||
return RenderFragment;
|
||||
|
||||
void RenderFragment(RenderTreeBuilder rtb)
|
||||
{
|
||||
rtb.OpenElement(1, "span");
|
||||
rtb.AddContent(2, parameters.GetValueOrDefault<string>("Value"));
|
||||
rtb.AddContent(2, content);
|
||||
rtb.CloseElement();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "A project template for creating a Blazor server app that runs server-side inside an ASP.NET Core app and handles user interactions over a SignalR connection. This template can be used for web apps with rich dynamic user interfaces (UIs).",
|
||||
"groupIdentity": "Microsoft.Web.Blazor.Server",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.Blazor.Server.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.Blazor.Server.CSharp.3.1",
|
||||
"shortName": "blazorserver",
|
||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
|
||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
"type": "project"
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
@*#if (NoAuth)
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
@*#if (!NoAuth)
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
#else
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
#endif*@
|
||||
</Found>
|
||||
<NotFound>
|
||||
@*#if (!NoAuth)
|
||||
<CascadingAuthenticationState>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</CascadingAuthenticationState>
|
||||
#else
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
#endif*@
|
||||
</NotFound>
|
||||
</Router>
|
||||
#else
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
#endif*@
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "An empty project template for creating an ASP.NET Core application. This template does not have any content in it.",
|
||||
"groupIdentity": "Microsoft.Web.Empty",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.Empty.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.Empty.CSharp.3.1",
|
||||
"shortName": "web",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "An empty project template for creating an ASP.NET Core application. This template does not have any content in it.",
|
||||
"groupIdentity": "Microsoft.Web.Empty",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.Empty.FSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.Empty.FSharp.3.1",
|
||||
"shortName": "web",
|
||||
"tags": {
|
||||
"language": "F#",
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "A project template for creating a gRPC ASP.NET Core service.",
|
||||
"groupIdentity": "Microsoft.Web.Grpc",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Grpc.Service.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Grpc.Service.CSharp.3.1",
|
||||
"shortName": "grpc",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "A project for creating a Razor class library that targets .NET Standard",
|
||||
"groupIdentity": "Microsoft.Web.Razor",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.Razor.Library.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.Razor.Library.CSharp.3.1",
|
||||
"shortName": "razorclasslib",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "A project template for creating an ASP.NET Core application with example ASP.NET Core Razor Pages content",
|
||||
"groupIdentity": "Microsoft.Web.RazorPages",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.RazorPages.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.RazorPages.CSharp.3.1",
|
||||
"shortName": [
|
||||
"webapp",
|
||||
"razor"
|
||||
],
|
||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
|
||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
"type": "project"
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "A project template for creating an ASP.NET Core application with example ASP.NET Core MVC Views and Controllers. This template can also be used for RESTful HTTP services.",
|
||||
"groupIdentity": "Microsoft.Web.Mvc",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.Mvc.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.Mvc.CSharp.3.1",
|
||||
"shortName": "mvc",
|
||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
|
||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
"type": "project"
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "A project template for creating an ASP.NET Core application with example ASP.NET Core MVC Views and Controllers. This template can also be used for RESTful HTTP services.",
|
||||
"groupIdentity": "Microsoft.Web.Mvc",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.Mvc.FSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.Mvc.FSharp.3.1",
|
||||
"shortName": "mvc",
|
||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
|
||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
|
||||
"tags": {
|
||||
"language": "F#",
|
||||
"type": "project"
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "A project template for creating an ASP.NET Core application with an example Controller for a RESTful HTTP service. This template can also be used for ASP.NET Core MVC Views and Controllers.",
|
||||
"groupIdentity": "Microsoft.Web.WebApi",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.WebApi.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.WebApi.CSharp.3.1",
|
||||
"shortName": "webapi",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "A project template for creating an ASP.NET Core application with an example Controller for a RESTful HTTP service. This template can also be used for ASP.NET Core MVC Views and Controllers.",
|
||||
"groupIdentity": "Microsoft.Web.WebApi",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Web.WebApi.FSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Web.WebApi.FSharp.3.1",
|
||||
"shortName": "webapi",
|
||||
"tags": {
|
||||
"language": "F#",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
"generatorVersions": "[1.0.0.0-*)",
|
||||
"description": "An empty project template for creating a worker service.",
|
||||
"groupIdentity": "Microsoft.Worker.Empty",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.Worker.Empty.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.Worker.Empty.CSharp.3.1",
|
||||
"shortName": "worker",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
"SPA"
|
||||
],
|
||||
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular.CSharp.3.1",
|
||||
"name": "ASP.NET Core with Angular",
|
||||
"preferNameDirectory": true,
|
||||
"primaryOutputs": [
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": ["styles.css"],
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": [],
|
||||
"assets": ["src/assets"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4750,9 +4750,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
||||
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
|
||||
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"neo-async": "^2.6.0",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
"SPA"
|
||||
],
|
||||
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React.CSharp.3.1",
|
||||
"name": "ASP.NET Core with React.js",
|
||||
"preferNameDirectory": true,
|
||||
"primaryOutputs": [
|
||||
|
|
|
|||
|
|
@ -4868,9 +4868,12 @@
|
|||
}
|
||||
},
|
||||
"eslint-utils": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
|
||||
"integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q=="
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
|
||||
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
|
||||
"requires": {
|
||||
"eslint-visitor-keys": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "1.0.0",
|
||||
|
|
@ -5664,9 +5667,9 @@
|
|||
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
||||
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
|
||||
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
|
||||
"requires": {
|
||||
"neo-async": "^2.6.0",
|
||||
"optimist": "^0.6.1",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
"SPA"
|
||||
],
|
||||
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux",
|
||||
"precedence": "5000",
|
||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux.CSharp.3.0",
|
||||
"precedence": "6000",
|
||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux.CSharp.3.1",
|
||||
"name": "ASP.NET Core with React.js and Redux",
|
||||
"preferNameDirectory": true,
|
||||
"primaryOutputs": [
|
||||
|
|
|
|||
|
|
@ -5044,9 +5044,12 @@
|
|||
}
|
||||
},
|
||||
"eslint-utils": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
|
||||
"integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q=="
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
|
||||
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
|
||||
"requires": {
|
||||
"eslint-visitor-keys": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "1.0.0",
|
||||
|
|
@ -5862,9 +5865,9 @@
|
|||
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
||||
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
|
||||
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
|
||||
"requires": {
|
||||
"neo-async": "^2.6.0",
|
||||
"optimist": "^0.6.1",
|
||||
|
|
|
|||
|
|
@ -104,14 +104,14 @@ namespace Templates.Test.Helpers
|
|||
return new ProcessEx(output, proc);
|
||||
}
|
||||
|
||||
public static async Task<ProcessEx> RunViaShellAsync(ITestOutputHelper output, string workingDirectory, string commandAndArgs)
|
||||
public static ProcessEx RunViaShell(ITestOutputHelper output, string workingDirectory, string commandAndArgs)
|
||||
{
|
||||
var (shellExe, argsPrefix) = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? ("cmd", "/c")
|
||||
: ("bash", "-c");
|
||||
|
||||
var result = Run(output, workingDirectory, shellExe, $"{argsPrefix} \"{commandAndArgs}\"");
|
||||
await result.Exited;
|
||||
result.WaitForExit(assertSuccess: false);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -168,9 +168,14 @@ namespace Templates.Test.Helpers
|
|||
return $"Process exited with code {_process.ExitCode}\nStdErr: {Error}\nStdOut: {Output}";
|
||||
}
|
||||
|
||||
public void WaitForExit(bool assertSuccess)
|
||||
public void WaitForExit(bool assertSuccess, TimeSpan? timeSpan = null)
|
||||
{
|
||||
Exited.Wait();
|
||||
if(!timeSpan.HasValue)
|
||||
{
|
||||
timeSpan = TimeSpan.FromSeconds(480);
|
||||
}
|
||||
|
||||
Exited.Wait(timeSpan.Value);
|
||||
|
||||
if (assertSuccess && _process.ExitCode != 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ namespace Templates.Test.Helpers
|
|||
try
|
||||
{
|
||||
output.WriteLine($"Restoring NPM packages in '{workingDirectory}' using npm...");
|
||||
var result = await ProcessEx.RunViaShellAsync(output, workingDirectory, "npm install");
|
||||
var result = ProcessEx.RunViaShell(output, workingDirectory, "npm install");
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue