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>
|
<Uri>https://github.com/aspnet/Blazor</Uri>
|
||||||
<Sha>348e050ecd9bd8924581afb677089ae5e2d5e508</Sha>
|
<Sha>348e050ecd9bd8924581afb677089ae5e2d5e508</Sha>
|
||||||
</Dependency>
|
</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>
|
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
|
||||||
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
|
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
|
||||||
</Dependency>
|
</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>
|
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
|
||||||
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
|
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
|
||||||
</Dependency>
|
</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>
|
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
|
||||||
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
|
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
|
||||||
</Dependency>
|
</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>
|
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
|
||||||
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
|
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
|
||||||
</Dependency>
|
</Dependency>
|
||||||
<Dependency Name="dotnet-ef" Version="3.1.0-preview1.19472.2">
|
<Dependency Name="dotnet-ef" Version="3.1.0-preview1.19472.2">
|
||||||
<Uri>https://github.com/aspnet/EntityFrameworkCore</Uri>
|
<Uri>https://github.com/aspnet/EntityFrameworkCore</Uri>
|
||||||
|
|
|
||||||
|
|
@ -163,10 +163,10 @@
|
||||||
<MicrosoftEntityFrameworkCoreToolsPackageVersion>3.1.0-preview1.19472.2</MicrosoftEntityFrameworkCoreToolsPackageVersion>
|
<MicrosoftEntityFrameworkCoreToolsPackageVersion>3.1.0-preview1.19472.2</MicrosoftEntityFrameworkCoreToolsPackageVersion>
|
||||||
<MicrosoftEntityFrameworkCorePackageVersion>3.1.0-preview1.19472.2</MicrosoftEntityFrameworkCorePackageVersion>
|
<MicrosoftEntityFrameworkCorePackageVersion>3.1.0-preview1.19472.2</MicrosoftEntityFrameworkCorePackageVersion>
|
||||||
<!-- Packages from aspnet/AspNetCore-Tooling -->
|
<!-- Packages from aspnet/AspNetCore-Tooling -->
|
||||||
<MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>3.1.0-preview1.19472.1</MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>
|
<MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>3.1.0-preview1.19501.1</MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>
|
||||||
<MicrosoftAspNetCoreRazorLanguagePackageVersion>3.1.0-preview1.19472.1</MicrosoftAspNetCoreRazorLanguagePackageVersion>
|
<MicrosoftAspNetCoreRazorLanguagePackageVersion>3.1.0-preview1.19501.1</MicrosoftAspNetCoreRazorLanguagePackageVersion>
|
||||||
<MicrosoftCodeAnalysisRazorPackageVersion>3.1.0-preview1.19472.1</MicrosoftCodeAnalysisRazorPackageVersion>
|
<MicrosoftCodeAnalysisRazorPackageVersion>3.1.0-preview1.19501.1</MicrosoftCodeAnalysisRazorPackageVersion>
|
||||||
<MicrosoftNETSdkRazorPackageVersion>3.1.0-preview1.19472.1</MicrosoftNETSdkRazorPackageVersion>
|
<MicrosoftNETSdkRazorPackageVersion>3.1.0-preview1.19501.1</MicrosoftNETSdkRazorPackageVersion>
|
||||||
</PropertyGroup>
|
</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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Analyzers
|
||||||
{
|
{
|
||||||
// ASP
|
// ASP
|
||||||
BuildServiceProviderShouldNotCalledInConfigureServicesMethod,
|
BuildServiceProviderShouldNotCalledInConfigureServicesMethod,
|
||||||
|
IncorrectlyConfiguredAuthorizationMiddleware,
|
||||||
|
|
||||||
// MVC
|
// MVC
|
||||||
UnsupportedUseMvcWithEndpointRouting,
|
UnsupportedUseMvcWithEndpointRouting,
|
||||||
|
|
@ -42,6 +43,15 @@ namespace Microsoft.AspNetCore.Analyzers
|
||||||
DiagnosticSeverity.Warning,
|
DiagnosticSeverity.Warning,
|
||||||
isEnabledByDefault: true,
|
isEnabledByDefault: true,
|
||||||
helpLinkUri: "https://aka.ms/YJggeFn");
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -82,6 +82,7 @@ namespace Microsoft.AspNetCore.Analyzers
|
||||||
var analysis = builder.Build();
|
var analysis = builder.Build();
|
||||||
new UseMvcAnalyzer(analysis).AnalyzeSymbol(context);
|
new UseMvcAnalyzer(analysis).AnalyzeSymbol(context);
|
||||||
new BuildServiceProviderValidator(analysis).AnalyzeSymbol(context);
|
new BuildServiceProviderValidator(analysis).AnalyzeSymbol(context);
|
||||||
|
new UseAuthorizationAnalyzer(analysis).AnalyzeSymbol(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
}, SymbolKind.NamedType);
|
}, 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(
|
Assert.Collection(
|
||||||
middlewareAnalysis.Middleware,
|
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("UseMiddleware", item.UseMethod.Name),
|
||||||
item => Assert.Equal("UseMvc", item.UseMethod.Name),
|
item => Assert.Equal("UseMvc", item.UseMethod.Name),
|
||||||
item => Assert.Equal("UseRouting", 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);
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
|
||||||
{
|
{
|
||||||
/*MM1*/app.UseMvcWithDefaultRoute();
|
/*MM1*/app.UseMvcWithDefaultRoute();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseStaticFiles();
|
||||||
app.UseMiddleware<AuthorizationMiddleware>();
|
app.UseMiddleware<AuthorizationMiddleware>();
|
||||||
|
|
||||||
/*MM2*/app.UseMvc();
|
/*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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
app.UseAuthorization();
|
app.UseStaticFiles();
|
||||||
app.UseMiddleware<AuthorizationMiddleware>();
|
app.UseMiddleware<AuthorizationMiddleware>();
|
||||||
|
|
||||||
/*MM*/app.UseMvc();
|
/*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))
|
if (_subscribers != null && ChangeDetection.MayHaveChanged(previousValue, Value))
|
||||||
{
|
{
|
||||||
NotifySubscribers();
|
NotifySubscribers(parameters.Lifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
@ -168,11 +168,11 @@ namespace Microsoft.AspNetCore.Components
|
||||||
_subscribers.Remove(subscriber);
|
_subscribers.Remove(subscriber);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NotifySubscribers()
|
private void NotifySubscribers(in ParameterViewLifetime lifetime)
|
||||||
{
|
{
|
||||||
foreach (var subscriber in _subscribers)
|
foreach (var subscriber in _subscribers)
|
||||||
{
|
{
|
||||||
subscriber.NotifyCascadingValueChanged();
|
subscriber.NotifyCascadingValueChanged(lifetime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Components.Reflection;
|
using Microsoft.AspNetCore.Components.Reflection;
|
||||||
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
using Microsoft.AspNetCore.Components.RenderTree;
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components
|
namespace Microsoft.AspNetCore.Components
|
||||||
|
|
@ -20,19 +21,21 @@ namespace Microsoft.AspNetCore.Components
|
||||||
RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
|
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 RenderTreeFrame[] _frames;
|
||||||
private readonly int _ownerIndex;
|
private readonly int _ownerIndex;
|
||||||
private readonly IReadOnlyList<CascadingParameterState> _cascadingParametersOrNull;
|
private readonly IReadOnlyList<CascadingParameterState> _cascadingParametersOrNull;
|
||||||
|
|
||||||
internal ParameterView(RenderTreeFrame[] frames, int ownerIndex)
|
internal ParameterView(in ParameterViewLifetime lifetime, RenderTreeFrame[] frames, int ownerIndex)
|
||||||
: this(frames, ownerIndex, null)
|
: 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;
|
_frames = frames;
|
||||||
_ownerIndex = ownerIndex;
|
_ownerIndex = ownerIndex;
|
||||||
_cascadingParametersOrNull = cascadingParametersOrNull;
|
_cascadingParametersOrNull = cascadingParametersOrNull;
|
||||||
|
|
@ -43,12 +46,17 @@ namespace Microsoft.AspNetCore.Components
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static ParameterView Empty => _empty;
|
public static ParameterView Empty => _empty;
|
||||||
|
|
||||||
|
internal ParameterViewLifetime Lifetime => _lifetime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns an enumerator that iterates through the <see cref="ParameterView"/>.
|
/// Returns an enumerator that iterates through the <see cref="ParameterView"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The enumerator.</returns>
|
/// <returns>The enumerator.</returns>
|
||||||
public Enumerator GetEnumerator()
|
public Enumerator GetEnumerator()
|
||||||
=> new Enumerator(_frames, _ownerIndex, _cascadingParametersOrNull);
|
{
|
||||||
|
_lifetime.AssertNotExpired();
|
||||||
|
return new Enumerator(_frames, _ownerIndex, _cascadingParametersOrNull);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the parameter with the specified name.
|
/// Gets the value of the parameter with the specified name.
|
||||||
|
|
@ -108,7 +116,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ParameterView WithCascadingParameters(IReadOnlyList<CascadingParameterState> cascadingParameters)
|
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
|
// 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
|
// 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);
|
frames[++i] = RenderTreeFrame.Attribute(i, kvp.Key, kvp.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ParameterView(frames, 0);
|
return new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -364,8 +364,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
||||||
// Handles the diff for attribute nodes only - this invariant is enforced by the caller.
|
// 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.
|
// 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*
|
// 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
|
// unordered. This is a case where we can produce a more minimal diff by avoiding
|
||||||
// non-meaningful reorderings of attributes.
|
// non-meaningful reorderings of attributes.
|
||||||
private static void AppendAttributeDiffEntriesForRange(
|
private static void AppendAttributeDiffEntriesForRange(
|
||||||
ref DiffContext diffContext,
|
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
|
// 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
|
// old parameter values if we wanted. By default, components always rerender
|
||||||
// after any SetParameters call, which is safe but now always optimal for perf.
|
// after any SetParameters call, which is safe but now always optimal for perf.
|
||||||
var oldParameters = new ParameterView(oldTree, oldComponentIndex);
|
var oldParameters = new ParameterView(ParameterViewLifetime.Unbound, oldTree, oldComponentIndex);
|
||||||
var newParameters = new ParameterView(newTree, newComponentIndex);
|
var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
|
||||||
|
var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex);
|
||||||
if (!newParameters.DefinitelyEquals(oldParameters))
|
if (!newParameters.DefinitelyEquals(oldParameters))
|
||||||
{
|
{
|
||||||
componentState.SetDirectParameters(newParameters);
|
componentState.SetDirectParameters(newParameters);
|
||||||
|
|
@ -893,7 +894,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
||||||
var childComponentState = frame.ComponentState;
|
var childComponentState = frame.ComponentState;
|
||||||
|
|
||||||
// Set initial parameters
|
// 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);
|
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
|
/// 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
|
/// constantly building up long lists of parameters. Is private to this class, so the
|
||||||
/// fact that it's a mutable struct is manageable.
|
/// fact that it's a mutable struct is manageable.
|
||||||
///
|
///
|
||||||
/// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable.
|
/// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private struct DiffContext
|
private struct DiffContext
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
_renderTreeBuilderPrevious.GetFrames(),
|
_renderTreeBuilderPrevious.GetFrames(),
|
||||||
CurrentRenderTree.GetFrames());
|
CurrentRenderTree.GetFrames());
|
||||||
batchBuilder.UpdatedComponentDiffs.Append(diff);
|
batchBuilder.UpdatedComponentDiffs.Append(diff);
|
||||||
|
batchBuilder.InvalidateParameterViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryDisposeInBatch(RenderBatchBuilder batchBuilder, out Exception exception)
|
public bool TryDisposeInBatch(RenderBatchBuilder batchBuilder, out Exception exception)
|
||||||
|
|
@ -130,8 +131,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
public void SetDirectParameters(ParameterView parameters)
|
public void SetDirectParameters(ParameterView parameters)
|
||||||
{
|
{
|
||||||
// Note: We should be careful to ensure that the framework never calls
|
// Note: We should be careful to ensure that the framework never calls
|
||||||
// IComponent.SetParameters directly elsewhere. We should only call it
|
// IComponent.SetParametersAsync directly elsewhere. We should only call it
|
||||||
// via ComponentState.SetParameters (or NotifyCascadingValueChanged below).
|
// via ComponentState.SetDirectParameters (or NotifyCascadingValueChanged below).
|
||||||
// If we bypass this, the component won't receive the cascading parameters nor
|
// If we bypass this, the component won't receive the cascading parameters nor
|
||||||
// will it update its snapshot of direct parameters.
|
// will it update its snapshot of direct parameters.
|
||||||
|
|
||||||
|
|
@ -156,10 +157,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
_renderer.AddToPendingTasks(Component.SetParametersAsync(parameters));
|
_renderer.AddToPendingTasks(Component.SetParametersAsync(parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NotifyCascadingValueChanged()
|
public void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime)
|
||||||
{
|
{
|
||||||
var directParams = _latestDirectParametersSnapshot != null
|
var directParams = _latestDirectParametersSnapshot != null
|
||||||
? new ParameterView(_latestDirectParametersSnapshot.Buffer, 0)
|
? new ParameterView(lifetime, _latestDirectParametersSnapshot.Buffer, 0)
|
||||||
: ParameterView.Empty;
|
: ParameterView.Empty;
|
||||||
var allParams = directParams.WithCascadingParameters(_cascadingParameters);
|
var allParams = directParams.WithCascadingParameters(_cascadingParameters);
|
||||||
var task = Component.SetParametersAsync(allParams);
|
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>
|
/// </summary>
|
||||||
internal class RenderBatchBuilder : IDisposable
|
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
|
// Primary result data
|
||||||
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
|
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
|
||||||
public ArrayBuilder<int> DisposedComponentIds { get; } = new ArrayBuilder<int>();
|
public ArrayBuilder<int> DisposedComponentIds { get; } = new ArrayBuilder<int>();
|
||||||
|
|
@ -31,6 +36,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
// Scratch data structure for understanding attribute diffs.
|
// Scratch data structure for understanding attribute diffs.
|
||||||
public Dictionary<string, int> AttributeDiffSet { get; } = new Dictionary<string, int>();
|
public Dictionary<string, int> AttributeDiffSet { get; } = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
public int ParameterViewValidityStamp => _parameterViewValidityStamp;
|
||||||
|
|
||||||
internal StackObjectPool<Dictionary<object, KeyedItemInfo>> KeyedItemInfoDictionaryPool { get; }
|
internal StackObjectPool<Dictionary<object, KeyedItemInfo>> KeyedItemInfoDictionaryPool { get; }
|
||||||
= new StackObjectPool<Dictionary<object, KeyedItemInfo>>(maxPreservedItems: 10, () => new Dictionary<object, KeyedItemInfo>());
|
= new StackObjectPool<Dictionary<object, KeyedItemInfo>>(maxPreservedItems: 10, () => new Dictionary<object, KeyedItemInfo>());
|
||||||
|
|
||||||
|
|
@ -58,6 +65,22 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
DisposedComponentIds.ToRange(),
|
DisposedComponentIds.ToRange(),
|
||||||
DisposedEventHandlerIds.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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
EditsBuffer.Dispose();
|
EditsBuffer.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,40 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
Assert.Equal("The value of IsFixed cannot be changed dynamically.", ex.Message);
|
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)
|
private static T FindComponent<T>(CapturedBatch batch, out int componentId)
|
||||||
{
|
{
|
||||||
var componentFrame = batch.ReferenceFrames.Single(
|
var componentFrame = batch.ReferenceFrames.Single(
|
||||||
|
|
@ -378,6 +412,8 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
|
|
||||||
class CascadingParameterConsumerComponent<T> : AutoRenderComponent
|
class CascadingParameterConsumerComponent<T> : AutoRenderComponent
|
||||||
{
|
{
|
||||||
|
private ParameterView lastParameterView;
|
||||||
|
|
||||||
public int NumSetParametersCalls { get; private set; }
|
public int NumSetParametersCalls { get; private set; }
|
||||||
public int NumRenders { 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)
|
public override async Task SetParametersAsync(ParameterView parameters)
|
||||||
{
|
{
|
||||||
|
lastParameterView = parameters;
|
||||||
NumSetParametersCalls++;
|
NumSetParametersCalls++;
|
||||||
await base.SetParametersAsync(parameters);
|
await base.SetParametersAsync(parameters);
|
||||||
}
|
}
|
||||||
|
|
@ -395,6 +432,13 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
NumRenders++;
|
NumRenders++;
|
||||||
builder.AddContent(0, $"CascadingParameter={CascadingParameter}; RegularParameter={RegularParameter}");
|
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>
|
class SecondCascadingParameterConsumerComponent<T1, T2> : CascadingParameterConsumerComponent<T1>
|
||||||
|
|
|
||||||
|
|
@ -673,7 +673,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
}
|
}
|
||||||
builder.CloseComponent();
|
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>();
|
var cascadingParameters = new List<CascadingParameterState>();
|
||||||
foreach (var kvp in _keyValuePairs)
|
foreach (var kvp in _keyValuePairs)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
RenderTreeFrame.ChildComponent(0, typeof(FakeComponent)).WithComponentSubtreeLength(1)
|
RenderTreeFrame.ChildComponent(0, typeof(FakeComponent)).WithComponentSubtreeLength(1)
|
||||||
};
|
};
|
||||||
var parameters = new ParameterView(frames, 0);
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(ToEnumerable(parameters));
|
Assert.Empty(ToEnumerable(parameters));
|
||||||
|
|
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(1)
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(1)
|
||||||
};
|
};
|
||||||
var parameters = new ParameterView(frames, 0);
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(ToEnumerable(parameters));
|
Assert.Empty(ToEnumerable(parameters));
|
||||||
|
|
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
// end of the owner's descendants
|
// end of the owner's descendants
|
||||||
RenderTreeFrame.Attribute(3, "orphaned attribute", "value")
|
RenderTreeFrame.Attribute(3, "orphaned attribute", "value")
|
||||||
};
|
};
|
||||||
var parameters = new ParameterView(frames, 0);
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(ToEnumerable(parameters),
|
Assert.Collection(ToEnumerable(parameters),
|
||||||
|
|
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
RenderTreeFrame.Element(3, "child element").WithElementSubtreeLength(2),
|
RenderTreeFrame.Element(3, "child element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(4, "child attribute", "some value")
|
RenderTreeFrame.Attribute(4, "child attribute", "some value")
|
||||||
};
|
};
|
||||||
var parameters = new ParameterView(frames, 0);
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(ToEnumerable(parameters),
|
Assert.Collection(ToEnumerable(parameters),
|
||||||
|
|
@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
var attribute1Value = new object();
|
var attribute1Value = new object();
|
||||||
var attribute2Value = new object();
|
var attribute2Value = new object();
|
||||||
var attribute3Value = 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.Element(0, "some element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value)
|
RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value)
|
||||||
|
|
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
public void CanTryGetNonExistingValue()
|
public void CanTryGetNonExistingValue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameters = new ParameterView(new[]
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
||||||
|
|
@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
public void CanTryGetExistingValueWithCorrectType()
|
public void CanTryGetExistingValueWithCorrectType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameters = new ParameterView(new[]
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(1, "my entry", "hello")
|
RenderTreeFrame.Attribute(1, "my entry", "hello")
|
||||||
|
|
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var myEntryValue = new object();
|
var myEntryValue = new object();
|
||||||
var parameters = new ParameterView(new[]
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
|
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
|
||||||
|
|
@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var myEntryValue = new object();
|
var myEntryValue = new object();
|
||||||
var parameters = new ParameterView(new[]
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
|
||||||
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
|
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
|
||||||
|
|
@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
public void CanGetValueOrDefault_WithNonExistingValue()
|
public void CanGetValueOrDefault_WithNonExistingValue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameters = new ParameterView(new[]
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
||||||
|
|
@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var explicitDefaultValue = new DateTime(2018, 3, 20);
|
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.Element(0, "some element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
RenderTreeFrame.Attribute(1, "some other entry", new object())
|
||||||
|
|
@ -226,7 +226,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
public void ThrowsIfTryGetExistingValueWithIncorrectType()
|
public void ThrowsIfTryGetExistingValueWithIncorrectType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameters = new ParameterView(new[]
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(1, "my entry", "hello")
|
RenderTreeFrame.Attribute(1, "my entry", "hello")
|
||||||
|
|
@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var entry2Value = new object();
|
var entry2Value = new object();
|
||||||
var parameters = new ParameterView(new[]
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
|
||||||
RenderTreeFrame.Attribute(0, "entry 1", "value 1"),
|
RenderTreeFrame.Attribute(0, "entry 1", "value 1"),
|
||||||
|
|
@ -304,7 +304,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var myEntryValue = new object();
|
var myEntryValue = new object();
|
||||||
var parameters = new ParameterView(new[]
|
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
|
||||||
{
|
{
|
||||||
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
|
||||||
RenderTreeFrame.Attribute(1, "unrelated value", new object())
|
RenderTreeFrame.Attribute(1, "unrelated value", new object())
|
||||||
|
|
@ -322,6 +322,32 @@ namespace Microsoft.AspNetCore.Components
|
||||||
Assert.Same(myEntryValue, result);
|
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)
|
private Action<ParameterValue> AssertParameter(string expectedName, object expectedValue, bool expectedIsCascading)
|
||||||
{
|
{
|
||||||
return parameter =>
|
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);
|
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
|
private class NoOpRenderer : Renderer
|
||||||
{
|
{
|
||||||
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
|
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
|
||||||
|
|
@ -4443,5 +4476,25 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
public new void ProcessPendingRender()
|
public new void ProcessPendingRender()
|
||||||
=> base.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;
|
var count = Descriptors.Count;
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var (componentType, sequence) = Descriptors[i];
|
var (componentType, parameters, sequence) = Descriptors[i];
|
||||||
await Renderer.AddComponentAsync(componentType, sequence.ToString());
|
await Renderer.AddComponentAsync(componentType, parameters, sequence.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.InitializationSucceeded(_logger);
|
Log.InitializationSucceeded(_logger);
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,11 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
{
|
{
|
||||||
public Type ComponentType { get; set; }
|
public Type ComponentType { get; set; }
|
||||||
|
|
||||||
|
public ParameterView Parameters { get; set; }
|
||||||
|
|
||||||
public int Sequence { get; set; }
|
public int Sequence { get; set; }
|
||||||
|
|
||||||
public void Deconstruct(out Type componentType, out int sequence)
|
public void Deconstruct(out Type componentType, out ParameterView parameters, out int sequence) =>
|
||||||
{
|
(componentType, sequence, parameters) = (ComponentType, Sequence, Parameters);
|
||||||
componentType = ComponentType;
|
|
||||||
sequence = Sequence;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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 " +
|
throw new InvalidOperationException(
|
||||||
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
|
"JavaScript interop calls cannot be issued at this time. This is because the component is being " +
|
||||||
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not " +
|
$"statically rendererd. When prerendering is enabled, JavaScript interop calls can only be performed " +
|
||||||
"attempted during prerendering or while the client is disconnected.");
|
$"during the OnAfterRenderAsync lifecycle method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.BeginInvokeJS(_logger, asyncHandle, identifier);
|
Log.BeginInvokeJS(_logger, asyncHandle, identifier);
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
return RenderRootComponentAsync(componentId);
|
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()
|
protected override void ProcessPendingRender()
|
||||||
{
|
{
|
||||||
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)
|
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.
|
// 'sequence' indicates the order in which this component got rendered on the server.
|
||||||
// 'assemblyName' the assembly name for the rendered component.
|
// 'assemblyName' the assembly name for the rendered component.
|
||||||
// 'type' the full type 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.
|
// '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>>"}))
|
// 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:
|
// Serialization:
|
||||||
// For a given response, MVC renders one or more markers in sequence, including a descriptor for each rendered
|
// 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 IDataProtector _dataProtector;
|
||||||
private readonly ILogger<ServerComponentDeserializer> _logger;
|
private readonly ILogger<ServerComponentDeserializer> _logger;
|
||||||
private readonly ServerComponentTypeCache _rootComponentTypeCache;
|
private readonly ServerComponentTypeCache _rootComponentTypeCache;
|
||||||
|
private readonly ComponentParameterDeserializer _parametersDeserializer;
|
||||||
|
|
||||||
public ServerComponentDeserializer(
|
public ServerComponentDeserializer(
|
||||||
IDataProtectionProvider dataProtectionProvider,
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
ILogger<ServerComponentDeserializer> logger,
|
ILogger<ServerComponentDeserializer> logger,
|
||||||
ServerComponentTypeCache rootComponentTypeCache)
|
ServerComponentTypeCache rootComponentTypeCache,
|
||||||
|
ComponentParameterDeserializer parametersDeserializer)
|
||||||
{
|
{
|
||||||
// When we protect the data we use a time-limited data protector with the
|
// When we protect the data we use a time-limited data protector with the
|
||||||
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
|
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
|
||||||
|
|
@ -74,6 +80,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_rootComponentTypeCache = rootComponentTypeCache;
|
_rootComponentTypeCache = rootComponentTypeCache;
|
||||||
|
_parametersDeserializer = parametersDeserializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
|
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
|
||||||
|
|
@ -176,9 +183,16 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_parametersDeserializer.TryDeserializeParameters(serverComponent.ParameterDefinitions, serverComponent.ParameterValues, out var parameters))
|
||||||
|
{
|
||||||
|
// TryDeserializeParameters does appropriate logging.
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
var componentDescriptor = new ComponentDescriptor
|
var componentDescriptor = new ComponentDescriptor
|
||||||
{
|
{
|
||||||
ComponentType = componentType,
|
ComponentType = componentType,
|
||||||
|
Parameters = parameters,
|
||||||
Sequence = serverComponent.Sequence
|
Sequence = serverComponent.Sequence
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<CircuitFactory>();
|
services.TryAddSingleton<CircuitFactory>();
|
||||||
services.TryAddSingleton<ServerComponentDeserializer>();
|
services.TryAddSingleton<ServerComponentDeserializer>();
|
||||||
services.TryAddSingleton<ServerComponentTypeCache>();
|
services.TryAddSingleton<ServerComponentTypeCache>();
|
||||||
|
services.TryAddSingleton<ComponentParameterDeserializer>();
|
||||||
|
services.TryAddSingleton<ComponentParametersTypeCache>();
|
||||||
services.TryAddSingleton<CircuitIdFactory>();
|
services.TryAddSingleton<CircuitIdFactory>();
|
||||||
|
|
||||||
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
|
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
|
|
||||||
<!-- Shared descriptor infrastructure with MVC -->
|
<!-- Shared descriptor infrastructure with MVC -->
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
<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\ServerComponentSerializationSettings.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -40,6 +41,45 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
Assert.Equal(0, deserializedDescriptor.Sequence);
|
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]
|
[Fact]
|
||||||
public void CanParseMultipleMarkers()
|
public void CanParseMultipleMarkers()
|
||||||
{
|
{
|
||||||
|
|
@ -60,6 +100,65 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
Assert.Equal(1, secondDescriptor.Sequence);
|
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]
|
[Fact]
|
||||||
public void DoesNotParseOutOfOrderMarkers()
|
public void DoesNotParseOutOfOrderMarkers()
|
||||||
{
|
{
|
||||||
|
|
@ -213,7 +312,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
|
|
||||||
private string SerializeComponent(string assembly, string type) =>
|
private string SerializeComponent(string assembly, string type) =>
|
||||||
JsonSerializer.Serialize(
|
JsonSerializer.Serialize(
|
||||||
new ServerComponent(0, assembly, type, Guid.NewGuid()),
|
new ServerComponent(0, assembly, type, Array.Empty<ComponentParameter>(), Array.Empty<object>(), Guid.NewGuid()),
|
||||||
ServerComponentSerializationSettings.JsonSerializationOptions);
|
ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
|
||||||
private ServerComponentDeserializer CreateServerComponentDeserializer()
|
private ServerComponentDeserializer CreateServerComponentDeserializer()
|
||||||
|
|
@ -221,7 +320,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
return new ServerComponentDeserializer(
|
return new ServerComponentDeserializer(
|
||||||
_ephemeralDataProtectionProvider,
|
_ephemeralDataProtectionProvider,
|
||||||
NullLogger<ServerComponentDeserializer>.Instance,
|
NullLogger<ServerComponentDeserializer>.Instance,
|
||||||
new ServerComponentTypeCache());
|
new ServerComponentTypeCache(),
|
||||||
|
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SerializeMarkers(ServerComponentMarker[] markers) =>
|
private string SerializeMarkers(ServerComponentMarker[] markers) =>
|
||||||
|
|
@ -233,7 +333,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
var markers = new ServerComponentMarker[types.Length];
|
var markers = new ServerComponentMarker[types.Length];
|
||||||
for (var i = 0; i < types.Length; i++)
|
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;
|
return markers;
|
||||||
|
|
@ -245,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
var markers = new ServerComponentMarker[types.Length];
|
var markers = new ServerComponentMarker[types.Length];
|
||||||
for (var i = 0; i < types.Length; i++)
|
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;
|
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'] = {
|
window['Browser'] = {
|
||||||
init: () => { },
|
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);
|
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) {
|
function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () => void, onError: (reason?: any) => void) {
|
||||||
const module = {} as typeof Module;
|
const module = {} as typeof Module;
|
||||||
const wasmBinaryFile = '_framework/wasm/mono.wasm';
|
const wasmBinaryFile = '_framework/wasm/mono.wasm';
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,15 @@ import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalCh
|
||||||
import { applyCaptureIdToElement } from './ElementReferenceCapture';
|
import { applyCaptureIdToElement } from './ElementReferenceCapture';
|
||||||
import { EventFieldInfo } from './EventFieldInfo';
|
import { EventFieldInfo } from './EventFieldInfo';
|
||||||
import { dispatchEvent } from './RendererEventDispatcher';
|
import { dispatchEvent } from './RendererEventDispatcher';
|
||||||
|
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
|
||||||
const selectValuePropname = '_blazorSelectValue';
|
const selectValuePropname = '_blazorSelectValue';
|
||||||
const sharedTemplateElemForParsing = document.createElement('template');
|
const sharedTemplateElemForParsing = document.createElement('template');
|
||||||
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||||
const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
|
const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
|
||||||
const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {};
|
const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {};
|
||||||
|
const internalAttributeNamePrefix = '__internal_';
|
||||||
|
const eventPreventDefaultAttributeNamePrefix = 'preventDefault_';
|
||||||
|
const eventStopPropagationAttributeNamePrefix = 'stopPropagation_';
|
||||||
|
|
||||||
export class BrowserRenderer {
|
export class BrowserRenderer {
|
||||||
private eventDelegator: EventDelegator;
|
private eventDelegator: EventDelegator;
|
||||||
|
|
@ -23,6 +27,11 @@ export class BrowserRenderer {
|
||||||
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => {
|
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => {
|
||||||
raiseEvent(event, this.browserRendererId, 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 {
|
public attachRootComponentToLogicalElement(componentId: number, element: LogicalElement): void {
|
||||||
|
|
@ -281,15 +290,10 @@ export class BrowserRenderer {
|
||||||
private applyAttribute(batch: RenderBatch, componentId: number, toDomElement: Element, attributeFrame: RenderTreeFrame) {
|
private applyAttribute(batch: RenderBatch, componentId: number, toDomElement: Element, attributeFrame: RenderTreeFrame) {
|
||||||
const frameReader = batch.frameReader;
|
const frameReader = batch.frameReader;
|
||||||
const attributeName = frameReader.attributeName(attributeFrame)!;
|
const attributeName = frameReader.attributeName(attributeFrame)!;
|
||||||
const browserRendererId = this.browserRendererId;
|
|
||||||
const eventHandlerId = frameReader.attributeEventHandlerId(attributeFrame);
|
const eventHandlerId = frameReader.attributeEventHandlerId(attributeFrame);
|
||||||
|
|
||||||
if (eventHandlerId) {
|
if (eventHandlerId) {
|
||||||
const firstTwoChars = attributeName.substring(0, 2);
|
const eventName = stripOnPrefix(attributeName);
|
||||||
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'.`);
|
|
||||||
}
|
|
||||||
this.eventDelegator.setListener(toDomElement, eventName, eventHandlerId, componentId);
|
this.eventDelegator.setListener(toDomElement, eventName, eventHandlerId, componentId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -310,8 +314,30 @@ export class BrowserRenderer {
|
||||||
return this.tryApplyValueProperty(batch, element, attributeFrame);
|
return this.tryApplyValueProperty(batch, element, attributeFrame);
|
||||||
case 'checked':
|
case 'checked':
|
||||||
return this.tryApplyCheckedProperty(batch, element, attributeFrame);
|
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;
|
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.
|
// as it adds noise to the DOM.
|
||||||
start.textContent = '!';
|
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 {
|
export class EventDelegator {
|
||||||
private static nextEventDelegatorId = 0;
|
private static nextEventDelegatorId = 0;
|
||||||
|
|
||||||
private eventsCollectionKey: string;
|
private readonly eventsCollectionKey: string;
|
||||||
|
|
||||||
|
private readonly afterClickCallbacks: ((event: MouseEvent) => void)[] = [];
|
||||||
|
|
||||||
private eventInfoStore: EventInfoStore;
|
private eventInfoStore: EventInfoStore;
|
||||||
|
|
||||||
|
|
@ -42,21 +44,18 @@ export class EventDelegator {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setListener(element: Element, eventName: string, eventHandlerId: number, renderingComponentId: number) {
|
public setListener(element: Element, eventName: string, eventHandlerId: number, renderingComponentId: number) {
|
||||||
// Ensure we have a place to store event info for this element
|
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
|
||||||
let infoForElement: EventHandlerInfosForElement = element[this.eventsCollectionKey];
|
const existingHandler = infoForElement.getHandler(eventName);
|
||||||
if (!infoForElement) {
|
|
||||||
infoForElement = element[this.eventsCollectionKey] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (infoForElement.hasOwnProperty(eventName)) {
|
if (existingHandler) {
|
||||||
// We can cheaply update the info on the existing object and don't need any other housekeeping
|
// We can cheaply update the info on the existing object and don't need any other housekeeping
|
||||||
const oldInfo = infoForElement[eventName];
|
// Note that this also takes care of updating the eventHandlerId on the existing handler object
|
||||||
this.eventInfoStore.update(oldInfo.eventHandlerId, eventHandlerId);
|
this.eventInfoStore.update(existingHandler.eventHandlerId, eventHandlerId);
|
||||||
} else {
|
} else {
|
||||||
// Go through the whole flow which might involve registering a new global handler
|
// Go through the whole flow which might involve registering a new global handler
|
||||||
const newInfo = { element, eventName, eventHandlerId, renderingComponentId };
|
const newInfo = { element, eventName, eventHandlerId, renderingComponentId };
|
||||||
this.eventInfoStore.add(newInfo);
|
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
|
// Looks like this event handler wasn't already disposed
|
||||||
// Remove the associated data from the DOM element
|
// Remove the associated data from the DOM element
|
||||||
const element = info.element;
|
const element = info.element;
|
||||||
if (element.hasOwnProperty(this.eventsCollectionKey)) {
|
const elementEventInfos = this.getEventHandlerInfosForElement(element, false);
|
||||||
const elementEventInfos: EventHandlerInfosForElement = element[this.eventsCollectionKey];
|
if (elementEventInfos) {
|
||||||
delete elementEventInfos[info.eventName];
|
elementEventInfos.removeHandler(info.eventName);
|
||||||
if (Object.getOwnPropertyNames(elementEventInfos).length === 0) {
|
|
||||||
delete element[this.eventsCollectionKey];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private onGlobalEvent(evt: Event) {
|
||||||
if (!(evt.target instanceof Element)) {
|
if (!(evt.target instanceof Element)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -88,22 +102,46 @@ export class EventDelegator {
|
||||||
let candidateElement = evt.target as Element | null;
|
let candidateElement = evt.target as Element | null;
|
||||||
let eventArgs: EventForDotNet<UIEventArgs> | null = null; // Populate lazily
|
let eventArgs: EventForDotNet<UIEventArgs> | null = null; // Populate lazily
|
||||||
const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
|
const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
|
||||||
|
let stopPropagationWasRequested = false;
|
||||||
while (candidateElement) {
|
while (candidateElement) {
|
||||||
if (candidateElement.hasOwnProperty(this.eventsCollectionKey)) {
|
const handlerInfos = this.getEventHandlerInfosForElement(candidateElement, false);
|
||||||
const handlerInfos: EventHandlerInfosForElement = candidateElement[this.eventsCollectionKey];
|
if (handlerInfos) {
|
||||||
if (handlerInfos.hasOwnProperty(evt.type)) {
|
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
|
// We are going to raise an event for this element, so prepare info needed by the .NET code
|
||||||
if (!eventArgs) {
|
if (!eventArgs) {
|
||||||
eventArgs = EventForDotNet.fromDOMEvent(evt);
|
eventArgs = EventForDotNet.fromDOMEvent(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlerInfo = handlerInfos[evt.type];
|
|
||||||
const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
|
const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
|
||||||
this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs, eventFieldInfo);
|
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;
|
this.infosByEventHandlerId[info.eventHandlerId] = info;
|
||||||
|
|
||||||
const eventName = info.eventName;
|
this.addGlobalListener(info.eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addGlobalListener(eventName: string) {
|
||||||
if (this.countByEventName.hasOwnProperty(eventName)) {
|
if (this.countByEventName.hasOwnProperty(eventName)) {
|
||||||
this.countByEventName[eventName]++;
|
this.countByEventName[eventName]++;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -168,14 +209,46 @@ class EventInfoStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventHandlerInfosForElement {
|
class EventHandlerInfosForElement {
|
||||||
// Although we *could* track multiple event handlers per (element, eventName) pair
|
// Although we *could* track multiple event handlers per (element, eventName) pair
|
||||||
// (since they have distinct eventHandlerId values), there's no point doing so because
|
// (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
|
// 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
|
// can only have one attribute with a given name, hence only one event handler with
|
||||||
// that name at any one time.
|
// that name at any one time.
|
||||||
// So to keep things simple, only track one EventHandlerInfo per (element, eventName)
|
// 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 {
|
interface EventHandlerInfo {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import '@dotnet/jsinterop';
|
import '@dotnet/jsinterop';
|
||||||
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
|
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
|
||||||
|
import { EventDelegator } from '../Rendering/EventDelegator';
|
||||||
|
|
||||||
let hasRegisteredNavigationInterception = false;
|
let hasEnabledNavigationInterception = false;
|
||||||
let hasRegisteredNavigationEventListeners = false;
|
let hasRegisteredNavigationEventListeners = false;
|
||||||
|
|
||||||
// Will be initialized once someone registers
|
// Will be initialized once someone registers
|
||||||
|
|
@ -28,21 +29,30 @@ function listenForNavigationEvents(callback: (uri: string, intercepted: boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableNavigationInterception() {
|
function enableNavigationInterception() {
|
||||||
if (hasRegisteredNavigationInterception) {
|
hasEnabledNavigationInterception = true;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
if (event.button !== 0 || eventHasSpecialKey(event)) {
|
||||||
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
|
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Intercept clicks on all <a> elements where the href is within the <base href> URI space
|
// 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
|
// 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';
|
const hrefAttributeName = 'href';
|
||||||
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
|
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
|
||||||
const targetAttributeValue = anchorTarget.getAttribute('target');
|
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.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 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 partial class WheelEventArgs : Microsoft.AspNetCore.Components.Web.MouseEventArgs
|
||||||
{
|
{
|
||||||
public WheelEventArgs() { }
|
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.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 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 partial class WheelEventArgs : Microsoft.AspNetCore.Components.Web.MouseEventArgs
|
||||||
{
|
{
|
||||||
public WheelEventArgs() { }
|
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>
|
<ItemGroup>
|
||||||
<!-- Shared descriptor infrastructure with MVC -->
|
<!-- Shared descriptor infrastructure with MVC -->
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
<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\ServerComponentSerializationSettings.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||||
</ItemGroup>
|
</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();
|
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.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 content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
|
||||||
var markers = ReadMarkers(content);
|
var markers = ReadMarkers(content);
|
||||||
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
|
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
|
||||||
|
|
@ -84,6 +85,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
};
|
};
|
||||||
Assert.Equal(expectedComponentSequence, componentSequence);
|
Assert.Equal(expectedComponentSequence, componentSequence);
|
||||||
|
|
||||||
|
|
@ -93,6 +96,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||||
Browser.Exists(By.CssSelector("h3.interactive"));
|
Browser.Exists(By.CssSelector("h3.interactive"));
|
||||||
var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
|
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.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)
|
private (ServerComponentMarker, ServerComponentMarker)[] ReadMarkers(string content)
|
||||||
|
|
|
||||||
|
|
@ -99,12 +99,102 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
Browser.Empty(GetLogLines);
|
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()
|
private string[] GetLogLines()
|
||||||
=> Browser.FindElement(By.TagName("textarea"))
|
=> Browser.FindElement(By.TagName("textarea"))
|
||||||
.GetAttribute("value")
|
.GetAttribute("value")
|
||||||
.Replace("\r\n", "\n")
|
.Replace("\r\n", "\n")
|
||||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
void ClearLog()
|
||||||
|
=> Browser.FindElement(By.Id("clear-log")).Click();
|
||||||
|
|
||||||
private void TriggerCustomBubblingEvent(string elementId, string eventName)
|
private void TriggerCustomBubblingEvent(string elementId, string eventName)
|
||||||
{
|
{
|
||||||
var jsExecutor = (IJavaScriptExecutor)Browser;
|
var jsExecutor = (IJavaScriptExecutor)Browser;
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,49 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
Browser.Equal(0, () => BrowserScrollY);
|
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
|
private long BrowserScrollY
|
||||||
{
|
{
|
||||||
get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY");
|
get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY");
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,67 @@
|
||||||
<h3 id="event-bubbling">Bubbling standard event</h3>
|
<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"))">
|
<div @onclick="@(() => LogEvent("parent onclick"))">
|
||||||
<button id="button-with-onclick" @onclick="@(() => LogEvent("target onclick"))">Button with onclick handler</button>
|
@* This element shows you can stop propagation even without necessarily also handling the event *@
|
||||||
<button id="button-without-onclick" >Button without onclick handler</button>
|
<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>
|
</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>
|
<h3>Bubbling custom event</h3>
|
||||||
|
|
||||||
<div onsneeze="@(new Action(() => LogEvent("parent onsneeze")))">
|
<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-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>
|
</div>
|
||||||
|
|
||||||
<h3>Non-bubbling standard event</h3>
|
<h3>Non-bubbling standard event</h3>
|
||||||
|
|
@ -23,8 +75,13 @@
|
||||||
<h3>Event log</h3>
|
<h3>Event log</h3>
|
||||||
|
|
||||||
<textarea readonly @bind="logValue"></textarea>
|
<textarea readonly @bind="logValue"></textarea>
|
||||||
|
<button id="clear-log" @onclick="@(() => { logValue = string.Empty; })">Clear log</button>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
bool intermediateStopPropagation;
|
||||||
|
bool targetStopPropagation;
|
||||||
|
bool ancestorPreventDefault;
|
||||||
|
bool preventOnKeyDown;
|
||||||
string logValue = string.Empty;
|
string logValue = string.Empty;
|
||||||
|
|
||||||
void LogEvent(string message)
|
void LogEvent(string message)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
protected override void OnAfterRender(bool firstRender)
|
protected override void OnAfterRender(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender && Name == null)
|
||||||
{
|
{
|
||||||
Name = "Alfred";
|
Name = "Alfred";
|
||||||
interactive = "interactive";
|
interactive = "interactive";
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
<li><NavLink href="/subdir/WithParameters/Name/Abc/LastName/McDef">With more parameters</NavLink></li>
|
<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/LongPage1">Long page 1</NavLink></li>
|
||||||
<li><NavLink href="/subdir/LongPage2">Long page 2</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>
|
<li><NavLink>Null href never matches</NavLink></li>
|
||||||
</ul>
|
</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.AddMvc();
|
||||||
services.AddCors(options =>
|
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();
|
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
|
// Mount the server-side Blazor app on /subdir
|
||||||
app.Map("/subdir", app =>
|
app.Map("/subdir", app =>
|
||||||
{
|
{
|
||||||
|
|
@ -52,12 +49,14 @@ namespace TestServer
|
||||||
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
|
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseCors();
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
endpoints.MapControllers();
|
endpoints.MapControllers();
|
||||||
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
|
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@
|
||||||
<p>Some content after</p>
|
<p>Some content after</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
@*
|
@*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
@ -68,9 +69,7 @@ namespace TestServer
|
||||||
</html>");
|
</html>");
|
||||||
var content = writer.ToString();
|
var content = writer.ToString();
|
||||||
ctx.Response.ContentLength = content.Length;
|
ctx.Response.ContentLength = content.Length;
|
||||||
using var responseWriter = new StreamWriter(ctx.Response.Body);
|
await ctx.Response.WriteAsync(content);
|
||||||
await responseWriter.WriteAsync(content);
|
|
||||||
await responseWriter.FlushAsync();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
}
|
}
|
||||||
public enum SameSiteMode
|
public enum SameSiteMode
|
||||||
{
|
{
|
||||||
|
Unspecified = -1,
|
||||||
None = 0,
|
None = 0,
|
||||||
Lax = 1,
|
Lax = 1,
|
||||||
Strict = 2,
|
Strict = 2,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if the client should include a cookie on "same-site" or "cross-site" requests.
|
/// 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>
|
/// </summary>
|
||||||
// This mirrors Microsoft.AspNetCore.Http.SameSiteMode
|
// This mirrors Microsoft.AspNetCore.Http.SameSiteMode
|
||||||
public enum SameSiteMode
|
public enum SameSiteMode
|
||||||
{
|
{
|
||||||
/// <summary>No SameSite field will be set, the client should follow its default cookie policy.</summary>
|
/// <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,
|
None = 0,
|
||||||
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
|
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
|
||||||
Lax,
|
Lax,
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,14 @@ namespace Microsoft.Net.Http.Headers
|
||||||
private const string SecureToken = "secure";
|
private const string SecureToken = "secure";
|
||||||
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
|
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
|
||||||
private const string SameSiteToken = "samesite";
|
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 SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
|
||||||
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.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 HttpOnlyToken = "httponly";
|
||||||
private const string SeparatorToken = "; ";
|
private const string SeparatorToken = "; ";
|
||||||
private const string EqualsToken = "=";
|
private const string EqualsToken = "=";
|
||||||
|
|
@ -36,6 +42,14 @@ namespace Microsoft.Net.Http.Headers
|
||||||
private StringSegment _name;
|
private StringSegment _name;
|
||||||
private StringSegment _value;
|
private StringSegment _value;
|
||||||
|
|
||||||
|
static SetCookieHeaderValue()
|
||||||
|
{
|
||||||
|
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
|
||||||
|
{
|
||||||
|
SuppressSameSiteNone = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SetCookieHeaderValue()
|
private SetCookieHeaderValue()
|
||||||
{
|
{
|
||||||
// Used by the parser to create a new instance of this type.
|
// 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 bool Secure { get; set; }
|
||||||
|
|
||||||
public SameSiteMode SameSite { get; set; }
|
public SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
|
||||||
|
|
||||||
public bool HttpOnly { get; set; }
|
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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var length = _name.Length + EqualsToken.Length + _value.Length;
|
var length = _name.Length + EqualsToken.Length + _value.Length;
|
||||||
|
|
||||||
string maxAge = null;
|
string maxAge = null;
|
||||||
|
string sameSite = null;
|
||||||
|
|
||||||
if (Expires.HasValue)
|
if (Expires.HasValue)
|
||||||
{
|
{
|
||||||
|
|
@ -129,9 +144,20 @@ namespace Microsoft.Net.Http.Headers
|
||||||
length += SeparatorToken.Length + SecureToken.Length;
|
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;
|
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,9 +166,9 @@ namespace Microsoft.Net.Http.Headers
|
||||||
length += SeparatorToken.Length + HttpOnlyToken.Length;
|
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, headerValue._name);
|
||||||
Append(ref span, EqualsToken);
|
Append(ref span, EqualsToken);
|
||||||
|
|
@ -180,9 +206,9 @@ namespace Microsoft.Net.Http.Headers
|
||||||
AppendSegment(ref span, SecureToken, null);
|
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)
|
if (headerValue.HttpOnly)
|
||||||
|
|
@ -248,9 +274,18 @@ namespace Microsoft.Net.Http.Headers
|
||||||
AppendSegment(builder, SecureToken, null);
|
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)
|
if (HttpOnly)
|
||||||
|
|
@ -302,7 +337,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
|
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)
|
private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue)
|
||||||
{
|
{
|
||||||
Contract.Requires(startIndex >= 0);
|
Contract.Requires(startIndex >= 0);
|
||||||
|
|
@ -437,25 +472,34 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
result.Secure = true;
|
result.Secure = true;
|
||||||
}
|
}
|
||||||
// samesite-av = "SameSite" / "SameSite=" samesite-value
|
// samesite-av = "SameSite=" samesite-value
|
||||||
// samesite-value = "Strict" / "Lax"
|
// samesite-value = "Strict" / "Lax" / "None"
|
||||||
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
|
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (!ReadEqualsSign(input, ref offset))
|
if (!ReadEqualsSign(input, ref offset))
|
||||||
{
|
{
|
||||||
result.SameSite = SameSiteMode.Strict;
|
result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.Unspecified;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
|
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;
|
result.SameSite = SameSiteMode.Lax;
|
||||||
}
|
}
|
||||||
|
else if (!SuppressSameSiteNone
|
||||||
|
&& StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.SameSite = SameSiteMode.None;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result.SameSite = SameSiteMode.Strict;
|
result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.Unspecified;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
SameSite = SameSiteMode.None,
|
SameSite = SameSiteMode.None,
|
||||||
};
|
};
|
||||||
dataset.Add(header7, "name7=value7");
|
dataset.Add(header7, "name7=value7; samesite=none");
|
||||||
|
|
||||||
|
|
||||||
return dataset;
|
return dataset;
|
||||||
|
|
@ -155,9 +155,20 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
SameSite = SameSiteMode.Strict
|
SameSite = SameSiteMode.Strict
|
||||||
};
|
};
|
||||||
var string6a = "name6=value6; samesite";
|
var string6 = "name6=value6; samesite=Strict";
|
||||||
var string6b = "name6=value6; samesite=Strict";
|
|
||||||
var string6c = "name6=value6; samesite=invalid";
|
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 }.ToList(), new[] { string1 });
|
||||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, 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[] { 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[] { string5a });
|
||||||
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
|
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
|
||||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6a });
|
dataset.Add(new[] { header6 }.ToList(), new[] { string6 });
|
||||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6b });
|
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
|
||||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6c });
|
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
|
||||||
|
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
|
||||||
|
|
||||||
return dataset;
|
return dataset;
|
||||||
}
|
}
|
||||||
|
|
@ -301,6 +313,28 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Assert.Equal(expectedValue, input.ToString());
|
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]
|
[Theory]
|
||||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
|
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
|
||||||
|
|
@ -312,6 +346,32 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Assert.Equal(expectedValue, builder.ToString());
|
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]
|
[Theory]
|
||||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
||||||
|
|
@ -322,6 +382,31 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Assert.Equal(expectedValue, header.ToString());
|
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]
|
[Theory]
|
||||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
||||||
|
|
@ -332,6 +417,31 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Assert.Equal(expectedValue, header.ToString());
|
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]
|
[Theory]
|
||||||
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
|
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)
|
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,20 @@ namespace Microsoft.AspNetCore.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CookieBuilder
|
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;
|
private string _name;
|
||||||
|
|
||||||
|
static CookieBuilder()
|
||||||
|
{
|
||||||
|
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
|
||||||
|
{
|
||||||
|
SuppressSameSiteNone = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the cookie.
|
/// The name of the cookie.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -49,12 +61,12 @@ namespace Microsoft.AspNetCore.Http
|
||||||
public virtual bool HttpOnly { get; set; }
|
public virtual bool HttpOnly { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Determines the value that will set on <seealso cref="CookieOptions.SameSite"/>.
|
/// Determines the value that will set on <seealso cref="CookieOptions.SameSite"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.None;
|
public virtual SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The policy that will be used to determine <seealso cref="CookieOptions.Secure"/>.
|
/// The policy that will be used to determine <seealso cref="CookieOptions.Secure"/>.
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Http
|
||||||
}
|
}
|
||||||
public enum SameSiteMode
|
public enum SameSiteMode
|
||||||
{
|
{
|
||||||
|
Unspecified = -1,
|
||||||
None = 0,
|
None = 0,
|
||||||
Lax = 1,
|
Lax = 1,
|
||||||
Strict = 2,
|
Strict = 2,
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Http
|
||||||
}
|
}
|
||||||
public enum SameSiteMode
|
public enum SameSiteMode
|
||||||
{
|
{
|
||||||
|
Unspecified = -1,
|
||||||
None = 0,
|
None = 0,
|
||||||
Lax = 1,
|
Lax = 1,
|
||||||
Strict = 2,
|
Strict = 2,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,18 @@ namespace Microsoft.AspNetCore.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CookieOptions
|
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>
|
/// <summary>
|
||||||
/// Creates a default cookie with a path of '/'.
|
/// Creates a default cookie with a path of '/'.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -43,10 +55,10 @@ namespace Microsoft.AspNetCore.Http
|
||||||
public bool Secure { get; set; }
|
public bool Secure { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <returns>The <see cref="SameSiteMode"/> representing the enforcement mode of the cookie.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script.
|
/// 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>
|
/// <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.
|
/// 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>
|
/// </summary>
|
||||||
// This mirrors Microsoft.Net.Http.Headers.SameSiteMode
|
// This mirrors Microsoft.Net.Http.Headers.SameSiteMode
|
||||||
public enum SameSiteMode
|
public enum SameSiteMode
|
||||||
{
|
{
|
||||||
/// <summary>No SameSite field will be set, the client should follow its default cookie policy.</summary>
|
/// <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,
|
None = 0,
|
||||||
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
|
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
|
||||||
Lax,
|
Lax,
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
|
||||||
EndProject
|
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}"
|
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
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -651,6 +679,8 @@ Global
|
||||||
{611794D2-EF3A-422A-A077-23E61C7ADE49} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
{611794D2-EF3A-422A-A077-23E61C7ADE49} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
||||||
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0} = {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}
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {85B5E151-2E9D-419C-83DD-0DDCF446C83A}
|
SolutionGuid = {85B5E151-2E9D-419C-83DD-0DDCF446C83A}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
|
@ -14,8 +13,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
internal sealed class EndpointMiddleware
|
internal sealed class EndpointMiddleware
|
||||||
{
|
{
|
||||||
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked";
|
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareWithEndpointInvoked";
|
||||||
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";
|
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
|
|
@ -91,7 +90,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " +
|
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " +
|
||||||
"but a middleware was not found that supports authorization." +
|
"but a middleware was not found that supports authorization." +
|
||||||
Environment.NewLine +
|
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)
|
private static void ThrowMissingCorsMiddlewareException(Endpoint endpoint)
|
||||||
|
|
@ -99,7 +98,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " +
|
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " +
|
||||||
"but a middleware was not found that supports CORS." +
|
"but a middleware was not found that supports CORS." +
|
||||||
Environment.NewLine +
|
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
|
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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Cors" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Routing" />
|
<Reference Include="Microsoft.AspNetCore.Routing" />
|
||||||
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = "Endpoint Test contains authorization metadata, but a middleware was not found that supports authorization." +
|
var expected = "Endpoint Test contains authorization metadata, but a middleware was not found that supports authorization." +
|
||||||
Environment.NewLine +
|
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
|
var httpContext = new DefaultHttpContext
|
||||||
{
|
{
|
||||||
RequestServices = new ServiceProvider()
|
RequestServices = new ServiceProvider()
|
||||||
|
|
@ -197,7 +198,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = "Endpoint Test contains CORS metadata, but a middleware was not found that supports CORS." +
|
var expected = "Endpoint Test contains CORS metadata, but a middleware was not found that supports CORS." +
|
||||||
Environment.NewLine +
|
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
|
var httpContext = new DefaultHttpContext
|
||||||
{
|
{
|
||||||
RequestServices = new ServiceProvider()
|
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
|
public class CorsMiddleware
|
||||||
{
|
{
|
||||||
// Property key is used by other systems, e.g. MVC, to check if CORS middleware has run
|
// Property key is used by other systems, e.g. MVC, to check if CORS middleware has run
|
||||||
private const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";
|
private const string CorsMiddlewareWithEndpointInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
|
||||||
private static readonly object CorsMiddlewareInvokedValue = new object();
|
private static readonly object CorsMiddlewareWithEndpointInvokedValue = new object();
|
||||||
|
|
||||||
private readonly Func<object, Task> OnResponseStartingDelegate = OnResponseStarting;
|
private readonly Func<object, Task> OnResponseStartingDelegate = OnResponseStarting;
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
|
|
@ -116,14 +116,6 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider)
|
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:
|
// CORS policy resolution rules:
|
||||||
//
|
//
|
||||||
// 1. If there is an endpoint with IDisableCorsAttribute then CORS is not run
|
// 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
|
// there is an endpoint with IEnableCorsAttribute that has a policy name then
|
||||||
// fetch policy by name, prioritizing it above policy on middleware
|
// fetch policy by name, prioritizing it above policy on middleware
|
||||||
// 3. If there is no policy on middleware then use name on middleware
|
// 3. If there is no policy on middleware then use name on middleware
|
||||||
|
|
||||||
var endpoint = context.GetEndpoint();
|
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
|
// Get the most significant CORS metadata for the endpoint
|
||||||
// For backwards compatibility reasons this is then downcast to Enable/Disable metadata
|
// For backwards compatibility reasons this is then downcast to Enable/Disable metadata
|
||||||
var corsMetadata = endpoint?.Metadata.GetMetadata<ICorsMetadata>();
|
var corsMetadata = endpoint?.Metadata.GetMetadata<ICorsMetadata>();
|
||||||
|
|
|
||||||
|
|
@ -918,7 +918,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
||||||
await middleware.Invoke(httpContext, mockProvider);
|
await middleware.Invoke(httpContext, mockProvider);
|
||||||
|
|
||||||
// Assert
|
// 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]
|
[Fact]
|
||||||
|
|
@ -936,12 +936,37 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
||||||
"DefaultPolicyName");
|
"DefaultPolicyName");
|
||||||
|
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint"));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await middleware.Invoke(httpContext, mockProvider);
|
await middleware.Invoke(httpContext, mockProvider);
|
||||||
|
|
||||||
// Assert
|
// 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 System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Diagnostics;
|
using Microsoft.AspNetCore.Diagnostics;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Builder
|
namespace Microsoft.AspNetCore.Builder
|
||||||
|
|
@ -188,6 +189,12 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
|
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.Path = newPath;
|
||||||
context.HttpContext.Request.QueryString = newQueryString;
|
context.HttpContext.Request.QueryString = newQueryString;
|
||||||
try
|
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]
|
[Fact]
|
||||||
public async Task ClearsCacheHeaders_SetByReexecutionPathHandlers()
|
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)
|
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 serviceProvider = context.RequestServices;
|
||||||
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
|
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
|
||||||
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
|
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
|
||||||
|
|
@ -106,6 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
var currentInvocation = invocationSerializer.SerializeInvocation(
|
var currentInvocation = invocationSerializer.SerializeInvocation(
|
||||||
invocationId,
|
invocationId,
|
||||||
type,
|
type,
|
||||||
|
parametersCollection,
|
||||||
prerendered: true);
|
prerendered: true);
|
||||||
|
|
||||||
var result = await prerenderer.PrerenderComponentAsync(
|
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)
|
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 serviceProvider = context.RequestServices;
|
||||||
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
|
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));
|
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
||||||
|
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -19,22 +19,27 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.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);
|
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (int sequence, string payload) CreateSerializedServerComponent(
|
private (int sequence, string payload) CreateSerializedServerComponent(
|
||||||
ServerComponentInvocationSequence invocationId,
|
ServerComponentInvocationSequence invocationId,
|
||||||
Type rootComponent)
|
Type rootComponent,
|
||||||
|
ParameterView parameters)
|
||||||
{
|
{
|
||||||
var sequence = invocationId.Next();
|
var sequence = invocationId.Next();
|
||||||
|
|
||||||
|
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
|
||||||
|
|
||||||
var serverComponent = new ServerComponent(
|
var serverComponent = new ServerComponent(
|
||||||
sequence,
|
sequence,
|
||||||
rootComponent.Assembly.GetName().Name,
|
rootComponent.Assembly.GetName().Name,
|
||||||
rootComponent.FullName,
|
rootComponent.FullName,
|
||||||
|
definitions,
|
||||||
|
values,
|
||||||
invocationId.Value);
|
invocationId.Value);
|
||||||
|
|
||||||
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
|
|
||||||
|
|
@ -188,25 +188,208 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
Assert.Equal("<p>Hello Steve!</p>", content);
|
Assert.Equal("<p>Hello Steve!</p>", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[InlineData(RenderMode.Server, "Server components with parameters are not supported.")]
|
public async Task CanRender_ComponentWithParameters_ServerMode()
|
||||||
[InlineData(RenderMode.ServerPrerendered, "Prerendering server components with parameters is not supported.")]
|
|
||||||
public async Task ComponentWithParametersObject_ThrowsInvalidOperationExceptionForServerRenderModes(
|
|
||||||
RenderMode renderMode,
|
|
||||||
string expectedMessage)
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var helper = CreateHelper();
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act & Assert
|
// Act
|
||||||
var result = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<GreetingComponent>(
|
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
||||||
renderMode,
|
RenderMode.Server,
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Name = "Steve"
|
Name = "Daniel"
|
||||||
}));
|
});
|
||||||
Assert.Equal(expectedMessage, result.Message);
|
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]
|
[Fact]
|
||||||
|
|
@ -547,7 +730,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
var s = 0;
|
var s = 0;
|
||||||
base.BuildRenderTree(builder);
|
base.BuildRenderTree(builder);
|
||||||
builder.OpenElement(s++, "p");
|
builder.OpenElement(s++, "p");
|
||||||
builder.AddContent(s++, $"Hello {Name}!");
|
builder.AddContent(s++, $"Hello {Name ?? ("(null)")}!");
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorComponents
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expectedHtml, result);
|
Assert.Equal(expectedHtml, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RenderComponentAsync_MarksSelectedOptionsAsSelected()
|
public void RenderComponentAsync_MarksSelectedOptionsAsSelected()
|
||||||
{
|
{
|
||||||
|
|
@ -668,18 +668,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorComponents
|
||||||
|
|
||||||
public Task SetParametersAsync(ParameterView parameters)
|
public Task SetParametersAsync(ParameterView parameters)
|
||||||
{
|
{
|
||||||
_renderHandle.Render(CreateRenderFragment(parameters));
|
var content = parameters.GetValueOrDefault<string>("Value");
|
||||||
|
_renderHandle.Render(CreateRenderFragment(content));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderFragment CreateRenderFragment(ParameterView parameters)
|
private RenderFragment CreateRenderFragment(string content)
|
||||||
{
|
{
|
||||||
return RenderFragment;
|
return RenderFragment;
|
||||||
|
|
||||||
void RenderFragment(RenderTreeBuilder rtb)
|
void RenderFragment(RenderTreeBuilder rtb)
|
||||||
{
|
{
|
||||||
rtb.OpenElement(1, "span");
|
rtb.OpenElement(1, "span");
|
||||||
rtb.AddContent(2, parameters.GetValueOrDefault<string>("Value"));
|
rtb.AddContent(2, content);
|
||||||
rtb.CloseElement();
|
rtb.CloseElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"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).",
|
"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",
|
"groupIdentity": "Microsoft.Web.Blazor.Server",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.Blazor.Server.CSharp.3.0",
|
"identity": "Microsoft.Web.Blazor.Server.CSharp.3.1",
|
||||||
"shortName": "blazorserver",
|
"shortName": "blazorserver",
|
||||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
|
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "C#",
|
"language": "C#",
|
||||||
"type": "project"
|
"type": "project"
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
<Router AppAssembly="@typeof(Program).Assembly">
|
@*#if (NoAuth)
|
||||||
|
<Router AppAssembly="@typeof(Program).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
@*#if (!NoAuth)
|
|
||||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
|
||||||
#else
|
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
#endif*@
|
|
||||||
</Found>
|
</Found>
|
||||||
<NotFound>
|
<NotFound>
|
||||||
@*#if (!NoAuth)
|
|
||||||
<CascadingAuthenticationState>
|
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
|
||||||
<p>Sorry, there's nothing at this address.</p>
|
|
||||||
</LayoutView>
|
|
||||||
</CascadingAuthenticationState>
|
|
||||||
#else
|
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
<p>Sorry, there's nothing at this address.</p>
|
<p>Sorry, there's nothing at this address.</p>
|
||||||
</LayoutView>
|
</LayoutView>
|
||||||
#endif*@
|
|
||||||
</NotFound>
|
</NotFound>
|
||||||
</Router>
|
</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-*)",
|
"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.",
|
"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",
|
"groupIdentity": "Microsoft.Web.Empty",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.Empty.CSharp.3.0",
|
"identity": "Microsoft.Web.Empty.CSharp.3.1",
|
||||||
"shortName": "web",
|
"shortName": "web",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "C#",
|
"language": "C#",
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"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.",
|
"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",
|
"groupIdentity": "Microsoft.Web.Empty",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.Empty.FSharp.3.0",
|
"identity": "Microsoft.Web.Empty.FSharp.3.1",
|
||||||
"shortName": "web",
|
"shortName": "web",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "F#",
|
"language": "F#",
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"generatorVersions": "[1.0.0.0-*)",
|
||||||
"description": "A project template for creating a gRPC ASP.NET Core service.",
|
"description": "A project template for creating a gRPC ASP.NET Core service.",
|
||||||
"groupIdentity": "Microsoft.Web.Grpc",
|
"groupIdentity": "Microsoft.Web.Grpc",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Grpc.Service.CSharp.3.0",
|
"identity": "Microsoft.Grpc.Service.CSharp.3.1",
|
||||||
"shortName": "grpc",
|
"shortName": "grpc",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "C#",
|
"language": "C#",
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"generatorVersions": "[1.0.0.0-*)",
|
||||||
"description": "A project for creating a Razor class library that targets .NET Standard",
|
"description": "A project for creating a Razor class library that targets .NET Standard",
|
||||||
"groupIdentity": "Microsoft.Web.Razor",
|
"groupIdentity": "Microsoft.Web.Razor",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.Razor.Library.CSharp.3.0",
|
"identity": "Microsoft.Web.Razor.Library.CSharp.3.1",
|
||||||
"shortName": "razorclasslib",
|
"shortName": "razorclasslib",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "C#",
|
"language": "C#",
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"generatorVersions": "[1.0.0.0-*)",
|
||||||
"description": "A project template for creating an ASP.NET Core application with example ASP.NET Core Razor Pages content",
|
"description": "A project template for creating an ASP.NET Core application with example ASP.NET Core Razor Pages content",
|
||||||
"groupIdentity": "Microsoft.Web.RazorPages",
|
"groupIdentity": "Microsoft.Web.RazorPages",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.RazorPages.CSharp.3.0",
|
"identity": "Microsoft.Web.RazorPages.CSharp.3.1",
|
||||||
"shortName": [
|
"shortName": [
|
||||||
"webapp",
|
"webapp",
|
||||||
"razor"
|
"razor"
|
||||||
],
|
],
|
||||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
|
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "C#",
|
"language": "C#",
|
||||||
"type": "project"
|
"type": "project"
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"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.",
|
"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",
|
"groupIdentity": "Microsoft.Web.Mvc",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.Mvc.CSharp.3.0",
|
"identity": "Microsoft.Web.Mvc.CSharp.3.1",
|
||||||
"shortName": "mvc",
|
"shortName": "mvc",
|
||||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
|
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "C#",
|
"language": "C#",
|
||||||
"type": "project"
|
"type": "project"
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"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.",
|
"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",
|
"groupIdentity": "Microsoft.Web.Mvc",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.Mvc.FSharp.3.0",
|
"identity": "Microsoft.Web.Mvc.FSharp.3.1",
|
||||||
"shortName": "mvc",
|
"shortName": "mvc",
|
||||||
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
|
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "F#",
|
"language": "F#",
|
||||||
"type": "project"
|
"type": "project"
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"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.",
|
"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",
|
"groupIdentity": "Microsoft.Web.WebApi",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.WebApi.CSharp.3.0",
|
"identity": "Microsoft.Web.WebApi.CSharp.3.1",
|
||||||
"shortName": "webapi",
|
"shortName": "webapi",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "C#",
|
"language": "C#",
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"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.",
|
"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",
|
"groupIdentity": "Microsoft.Web.WebApi",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Web.WebApi.FSharp.3.0",
|
"identity": "Microsoft.Web.WebApi.FSharp.3.1",
|
||||||
"shortName": "webapi",
|
"shortName": "webapi",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "F#",
|
"language": "F#",
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
"generatorVersions": "[1.0.0.0-*)",
|
"generatorVersions": "[1.0.0.0-*)",
|
||||||
"description": "An empty project template for creating a worker service.",
|
"description": "An empty project template for creating a worker service.",
|
||||||
"groupIdentity": "Microsoft.Worker.Empty",
|
"groupIdentity": "Microsoft.Worker.Empty",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.Worker.Empty.CSharp.3.0",
|
"identity": "Microsoft.Worker.Empty.CSharp.3.1",
|
||||||
"shortName": "worker",
|
"shortName": "worker",
|
||||||
"tags": {
|
"tags": {
|
||||||
"language": "C#",
|
"language": "C#",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
"SPA"
|
"SPA"
|
||||||
],
|
],
|
||||||
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular",
|
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular.CSharp.3.0",
|
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular.CSharp.3.1",
|
||||||
"name": "ASP.NET Core with Angular",
|
"name": "ASP.NET Core with Angular",
|
||||||
"preferNameDirectory": true,
|
"preferNameDirectory": true,
|
||||||
"primaryOutputs": [
|
"primaryOutputs": [
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "src/tsconfig.spec.json",
|
"tsConfig": "src/tsconfig.spec.json",
|
||||||
"karmaConfig": "src/karma.conf.js",
|
"karmaConfig": "src/karma.conf.js",
|
||||||
"styles": ["styles.css"],
|
"styles": ["src/styles.css"],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"assets": ["src/assets"]
|
"assets": ["src/assets"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4750,9 +4750,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
"version": "4.1.2",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
|
||||||
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"neo-async": "^2.6.0",
|
"neo-async": "^2.6.0",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
"SPA"
|
"SPA"
|
||||||
],
|
],
|
||||||
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React",
|
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React.CSharp.3.0",
|
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React.CSharp.3.1",
|
||||||
"name": "ASP.NET Core with React.js",
|
"name": "ASP.NET Core with React.js",
|
||||||
"preferNameDirectory": true,
|
"preferNameDirectory": true,
|
||||||
"primaryOutputs": [
|
"primaryOutputs": [
|
||||||
|
|
|
||||||
|
|
@ -4868,9 +4868,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-utils": {
|
"eslint-utils": {
|
||||||
"version": "1.3.1",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
|
||||||
"integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q=="
|
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
|
||||||
|
"requires": {
|
||||||
|
"eslint-visitor-keys": "^1.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"eslint-visitor-keys": {
|
"eslint-visitor-keys": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -5664,9 +5667,9 @@
|
||||||
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
|
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
"version": "4.1.2",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
|
||||||
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"neo-async": "^2.6.0",
|
"neo-async": "^2.6.0",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
"SPA"
|
"SPA"
|
||||||
],
|
],
|
||||||
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux",
|
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux",
|
||||||
"precedence": "5000",
|
"precedence": "6000",
|
||||||
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux.CSharp.3.0",
|
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux.CSharp.3.1",
|
||||||
"name": "ASP.NET Core with React.js and Redux",
|
"name": "ASP.NET Core with React.js and Redux",
|
||||||
"preferNameDirectory": true,
|
"preferNameDirectory": true,
|
||||||
"primaryOutputs": [
|
"primaryOutputs": [
|
||||||
|
|
|
||||||
|
|
@ -5044,9 +5044,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-utils": {
|
"eslint-utils": {
|
||||||
"version": "1.3.1",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
|
||||||
"integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q=="
|
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
|
||||||
|
"requires": {
|
||||||
|
"eslint-visitor-keys": "^1.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"eslint-visitor-keys": {
|
"eslint-visitor-keys": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -5862,9 +5865,9 @@
|
||||||
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
|
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
"version": "4.1.2",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
|
||||||
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"neo-async": "^2.6.0",
|
"neo-async": "^2.6.0",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
|
|
|
||||||
|
|
@ -104,14 +104,14 @@ namespace Templates.Test.Helpers
|
||||||
return new ProcessEx(output, proc);
|
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)
|
var (shellExe, argsPrefix) = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
? ("cmd", "/c")
|
? ("cmd", "/c")
|
||||||
: ("bash", "-c");
|
: ("bash", "-c");
|
||||||
|
|
||||||
var result = Run(output, workingDirectory, shellExe, $"{argsPrefix} \"{commandAndArgs}\"");
|
var result = Run(output, workingDirectory, shellExe, $"{argsPrefix} \"{commandAndArgs}\"");
|
||||||
await result.Exited;
|
result.WaitForExit(assertSuccess: false);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,9 +168,14 @@ namespace Templates.Test.Helpers
|
||||||
return $"Process exited with code {_process.ExitCode}\nStdErr: {Error}\nStdOut: {Output}";
|
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)
|
if (assertSuccess && _process.ExitCode != 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,7 @@ namespace Templates.Test.Helpers
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output.WriteLine($"Restoring NPM packages in '{workingDirectory}' using npm...");
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue