Merge branch 'release/3.1-preview1' => 'release/3.1' (#14645)

This commit is contained in:
Doug Bunting 2019-10-02 10:08:05 -07:00 committed by GitHub
commit 43456141e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
171 changed files with 4724 additions and 672 deletions

View File

@ -13,21 +13,21 @@
<Uri>https://github.com/aspnet/Blazor</Uri>
<Sha>348e050ecd9bd8924581afb677089ae5e2d5e508</Sha>
</Dependency>
<Dependency Name="Microsoft.AspNetCore.Razor.Language" Version="3.1.0-preview1.19472.1">
<Dependency Name="Microsoft.AspNetCore.Razor.Language" Version="3.1.0-preview1.19501.1">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
</Dependency>
<Dependency Name="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="3.1.0-preview1.19472.1">
<Dependency Name="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="3.1.0-preview1.19501.1">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.Razor" Version="3.1.0-preview1.19472.1">
<Dependency Name="Microsoft.CodeAnalysis.Razor" Version="3.1.0-preview1.19501.1">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
</Dependency>
<Dependency Name="Microsoft.NET.Sdk.Razor" Version="3.1.0-preview1.19472.1">
<Dependency Name="Microsoft.NET.Sdk.Razor" Version="3.1.0-preview1.19501.1">
<Uri>https://github.com/aspnet/AspNetCore-Tooling</Uri>
<Sha>9dc38f98bd6eb330aa1463c38bb2f6c6eccdb309</Sha>
<Sha>1e85487b5011a3541c78be97baa4407abf87ea1a</Sha>
</Dependency>
<Dependency Name="dotnet-ef" Version="3.1.0-preview1.19472.2">
<Uri>https://github.com/aspnet/EntityFrameworkCore</Uri>

View File

@ -163,10 +163,10 @@
<MicrosoftEntityFrameworkCoreToolsPackageVersion>3.1.0-preview1.19472.2</MicrosoftEntityFrameworkCoreToolsPackageVersion>
<MicrosoftEntityFrameworkCorePackageVersion>3.1.0-preview1.19472.2</MicrosoftEntityFrameworkCorePackageVersion>
<!-- Packages from aspnet/AspNetCore-Tooling -->
<MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>3.1.0-preview1.19472.1</MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>
<MicrosoftAspNetCoreRazorLanguagePackageVersion>3.1.0-preview1.19472.1</MicrosoftAspNetCoreRazorLanguagePackageVersion>
<MicrosoftCodeAnalysisRazorPackageVersion>3.1.0-preview1.19472.1</MicrosoftCodeAnalysisRazorPackageVersion>
<MicrosoftNETSdkRazorPackageVersion>3.1.0-preview1.19472.1</MicrosoftNETSdkRazorPackageVersion>
<MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>3.1.0-preview1.19501.1</MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion>
<MicrosoftAspNetCoreRazorLanguagePackageVersion>3.1.0-preview1.19501.1</MicrosoftAspNetCoreRazorLanguagePackageVersion>
<MicrosoftCodeAnalysisRazorPackageVersion>3.1.0-preview1.19501.1</MicrosoftCodeAnalysisRazorPackageVersion>
<MicrosoftNETSdkRazorPackageVersion>3.1.0-preview1.19501.1</MicrosoftNETSdkRazorPackageVersion>
</PropertyGroup>
<!--

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Analyzers
{
// ASP
BuildServiceProviderShouldNotCalledInConfigureServicesMethod,
IncorrectlyConfiguredAuthorizationMiddleware,
// MVC
UnsupportedUseMvcWithEndpointRouting,
@ -42,6 +43,15 @@ namespace Microsoft.AspNetCore.Analyzers
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/YJggeFn");
internal readonly static DiagnosticDescriptor IncorrectlyConfiguredAuthorizationMiddleware = new DiagnosticDescriptor(
"ASP0001",
"Authorization middleware is incorrectly configured.",
"The call to UseAuthorization should appear between app.UseRouting() and app.UseEndpoints(..) for authorization to be correctly evaluated.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/AA64fv1");
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -82,6 +82,7 @@ namespace Microsoft.AspNetCore.Analyzers
var analysis = builder.Build();
new UseMvcAnalyzer(analysis).AnalyzeSymbol(context);
new BuildServiceProviderValidator(analysis).AnalyzeSymbol(context);
new UseAuthorizationAnalyzer(analysis).AnalyzeSymbol(context);
});
}, SymbolKind.NamedType);

View File

@ -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;
}
}
}
}
}
}

View File

@ -162,7 +162,7 @@ namespace Microsoft.AspNetCore.Analyzers
Assert.Collection(
middlewareAnalysis.Middleware,
item => Assert.Equal("UseAuthorization", item.UseMethod.Name),
item => Assert.Equal("UseStaticFiles", item.UseMethod.Name),
item => Assert.Equal("UseMiddleware", item.UseMethod.Name),
item => Assert.Equal("UseMvc", item.UseMethod.Name),
item => Assert.Equal("UseRouting", item.UseMethod.Name),
@ -228,5 +228,90 @@ namespace Microsoft.AspNetCore.Analyzers
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic.Location);
});
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredCorrectly_ReportsNoDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthConfiguredCorrectly));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationInvokedMultipleTimesInEndpointRoutingBlock_ReportsNoDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthMultipleTimes));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredBeforeUseRouting_ReportsDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthBeforeUseRouting));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Collection(diagnostics,
diagnostic =>
{
Assert.Same(StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
});
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredAfterUseEndpoints_ReportsDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthAfterUseEndpoints));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Collection(diagnostics,
diagnostic =>
{
Assert.Same(StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
});
}
[Fact]
public async Task StartupAnalyzer_MultipleUseAuthorization_ReportsNoDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthFallbackPolicy));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder;

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Authorization;
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
/*MM1*/app.UseMvcWithDefaultRoute();
app.UseAuthorization();
app.UseStaticFiles();
app.UseMiddleware<AuthorizationMiddleware>();
/*MM2*/app.UseMvc();

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Authorization;
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
public void Configure(IApplicationBuilder app)
{
app.UseAuthorization();
app.UseStaticFiles();
app.UseMiddleware<AuthorizationMiddleware>();
/*MM*/app.UseMvc();

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
public class UseAuthAfterUseEndpoints
{
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(r => { });
/*MM*/app.UseAuthorization();
}
}
}

View File

@ -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 => { });
}
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
public class UseAuthConfiguredCorrectly
{
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(r => { });
}
}
}

View File

@ -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 => { });
}
}
}

View File

@ -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 => { });
}
}
}

View File

@ -127,7 +127,7 @@ namespace Microsoft.AspNetCore.Components
if (_subscribers != null && ChangeDetection.MayHaveChanged(previousValue, Value))
{
NotifySubscribers();
NotifySubscribers(parameters.Lifetime);
}
return Task.CompletedTask;
@ -168,11 +168,11 @@ namespace Microsoft.AspNetCore.Components
_subscribers.Remove(subscriber);
}
private void NotifySubscribers()
private void NotifySubscribers(in ParameterViewLifetime lifetime)
{
foreach (var subscriber in _subscribers)
{
subscriber.NotifyCascadingValueChanged();
subscriber.NotifyCascadingValueChanged(lifetime);
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components
@ -20,19 +21,21 @@ namespace Microsoft.AspNetCore.Components
RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
};
private static readonly ParameterView _empty = new ParameterView(_emptyFrames, 0, null);
private static readonly ParameterView _empty = new ParameterView(ParameterViewLifetime.Unbound, _emptyFrames, 0, null);
private readonly ParameterViewLifetime _lifetime;
private readonly RenderTreeFrame[] _frames;
private readonly int _ownerIndex;
private readonly IReadOnlyList<CascadingParameterState> _cascadingParametersOrNull;
internal ParameterView(RenderTreeFrame[] frames, int ownerIndex)
: this(frames, ownerIndex, null)
internal ParameterView(in ParameterViewLifetime lifetime, RenderTreeFrame[] frames, int ownerIndex)
: this(lifetime, frames, ownerIndex, null)
{
}
private ParameterView(RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList<CascadingParameterState> cascadingParametersOrNull)
private ParameterView(in ParameterViewLifetime lifetime, RenderTreeFrame[] frames, int ownerIndex, IReadOnlyList<CascadingParameterState> cascadingParametersOrNull)
{
_lifetime = lifetime;
_frames = frames;
_ownerIndex = ownerIndex;
_cascadingParametersOrNull = cascadingParametersOrNull;
@ -43,12 +46,17 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static ParameterView Empty => _empty;
internal ParameterViewLifetime Lifetime => _lifetime;
/// <summary>
/// Returns an enumerator that iterates through the <see cref="ParameterView"/>.
/// </summary>
/// <returns>The enumerator.</returns>
public Enumerator GetEnumerator()
=> new Enumerator(_frames, _ownerIndex, _cascadingParametersOrNull);
{
_lifetime.AssertNotExpired();
return new Enumerator(_frames, _ownerIndex, _cascadingParametersOrNull);
}
/// <summary>
/// Gets the value of the parameter with the specified name.
@ -108,7 +116,7 @@ namespace Microsoft.AspNetCore.Components
}
internal ParameterView WithCascadingParameters(IReadOnlyList<CascadingParameterState> cascadingParameters)
=> new ParameterView(_frames, _ownerIndex, cascadingParameters);
=> new ParameterView(_lifetime, _frames, _ownerIndex, cascadingParameters);
// It's internal because there isn't a known use case for user code comparing
// ParameterView instances, and even if there was, it's unlikely it should
@ -215,7 +223,7 @@ namespace Microsoft.AspNetCore.Components
frames[++i] = RenderTreeFrame.Attribute(i, kvp.Key, kvp.Value);
}
return new ParameterView(frames, 0);
return new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
}
/// <summary>

View File

@ -364,8 +364,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
// Handles the diff for attribute nodes only - this invariant is enforced by the caller.
//
// The diff for attributes is different because we allow attributes to appear in any order.
// Put another way, the attributes list of an element or component is *conceptually*
// unordered. This is a case where we can produce a more minimal diff by avoiding
// Put another way, the attributes list of an element or component is *conceptually*
// unordered. This is a case where we can produce a more minimal diff by avoiding
// non-meaningful reorderings of attributes.
private static void AppendAttributeDiffEntriesForRange(
ref DiffContext diffContext,
@ -519,8 +519,9 @@ namespace Microsoft.AspNetCore.Components.RenderTree
// comparisons it wants with the old values. Later we could choose to pass the
// old parameter values if we wanted. By default, components always rerender
// after any SetParameters call, which is safe but now always optimal for perf.
var oldParameters = new ParameterView(oldTree, oldComponentIndex);
var newParameters = new ParameterView(newTree, newComponentIndex);
var oldParameters = new ParameterView(ParameterViewLifetime.Unbound, oldTree, oldComponentIndex);
var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex);
if (!newParameters.DefinitelyEquals(oldParameters))
{
componentState.SetDirectParameters(newParameters);
@ -893,7 +894,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
var childComponentState = frame.ComponentState;
// Set initial parameters
var initialParameters = new ParameterView(frames, frameIndex);
var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
var initialParameters = new ParameterView(initialParametersLifetime, frames, frameIndex);
childComponentState.SetDirectParameters(initialParameters);
}
@ -957,7 +959,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
/// Exists only so that the various methods in this class can call each other without
/// constantly building up long lists of parameters. Is private to this class, so the
/// fact that it's a mutable struct is manageable.
///
///
/// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable.
/// </summary>
private struct DiffContext

View File

@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
_renderTreeBuilderPrevious.GetFrames(),
CurrentRenderTree.GetFrames());
batchBuilder.UpdatedComponentDiffs.Append(diff);
batchBuilder.InvalidateParameterViews();
}
public bool TryDisposeInBatch(RenderBatchBuilder batchBuilder, out Exception exception)
@ -130,8 +131,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
public void SetDirectParameters(ParameterView parameters)
{
// Note: We should be careful to ensure that the framework never calls
// IComponent.SetParameters directly elsewhere. We should only call it
// via ComponentState.SetParameters (or NotifyCascadingValueChanged below).
// IComponent.SetParametersAsync directly elsewhere. We should only call it
// via ComponentState.SetDirectParameters (or NotifyCascadingValueChanged below).
// If we bypass this, the component won't receive the cascading parameters nor
// will it update its snapshot of direct parameters.
@ -156,10 +157,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
_renderer.AddToPendingTasks(Component.SetParametersAsync(parameters));
}
public void NotifyCascadingValueChanged()
public void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime)
{
var directParams = _latestDirectParametersSnapshot != null
? new ParameterView(_latestDirectParametersSnapshot.Buffer, 0)
? new ParameterView(lifetime, _latestDirectParametersSnapshot.Buffer, 0)
: ParameterView.Empty;
var allParams = directParams.WithCascadingParameters(_cascadingParameters);
var task = Component.SetParametersAsync(allParams);

View File

@ -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.");
}
}
}
}

View File

@ -15,6 +15,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </summary>
internal class RenderBatchBuilder : IDisposable
{
// A value that, if changed, causes expiry of all ParameterView instances issued
// for this RenderBatchBuilder. This is to prevent invalid reads from arrays that
// may have been returned to the shared pool.
private int _parameterViewValidityStamp;
// Primary result data
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
public ArrayBuilder<int> DisposedComponentIds { get; } = new ArrayBuilder<int>();
@ -31,6 +36,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Scratch data structure for understanding attribute diffs.
public Dictionary<string, int> AttributeDiffSet { get; } = new Dictionary<string, int>();
public int ParameterViewValidityStamp => _parameterViewValidityStamp;
internal StackObjectPool<Dictionary<object, KeyedItemInfo>> KeyedItemInfoDictionaryPool { get; }
= new StackObjectPool<Dictionary<object, KeyedItemInfo>>(maxPreservedItems: 10, () => new Dictionary<object, KeyedItemInfo>());
@ -58,6 +65,22 @@ namespace Microsoft.AspNetCore.Components.Rendering
DisposedComponentIds.ToRange(),
DisposedEventHandlerIds.ToRange());
public void InvalidateParameterViews()
{
// Wrapping is fine because all that matters is whether a snapshotted value matches
// the current one. There's no plausible case where it wraps around and happens to
// increment all the way back to a previously-snapshotted value on the exact same
// call that's checking the value.
if (_parameterViewValidityStamp == int.MaxValue)
{
_parameterViewValidityStamp = int.MinValue;
}
else
{
_parameterViewValidityStamp++;
}
}
public void Dispose()
{
EditsBuffer.Dispose();

View File

@ -354,6 +354,40 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal("The value of IsFixed cannot be changed dynamically.", ex.Message);
}
[Fact]
public void ParameterViewSuppliedWithCascadingParametersCannotBeUsedAfterSynchronousReturn()
{
// Arrange
var providedValue = "Initial value";
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", providedValue);
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Initial render; capture nested component
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
// Re-render CascadingValue with new value, so it gets a new ParameterView
providedValue = "Updated value";
component.TriggerRender();
Assert.Equal(2, renderer.Batches.Count);
// It's no longer able to access anything in the ParameterView it just received
var ex = Assert.Throws<InvalidOperationException>(nestedComponent.AttemptIllegalAccessToLastParameterView);
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
}
private static T FindComponent<T>(CapturedBatch batch, out int componentId)
{
var componentFrame = batch.ReferenceFrames.Single(
@ -378,6 +412,8 @@ namespace Microsoft.AspNetCore.Components.Test
class CascadingParameterConsumerComponent<T> : AutoRenderComponent
{
private ParameterView lastParameterView;
public int NumSetParametersCalls { get; private set; }
public int NumRenders { get; private set; }
@ -386,6 +422,7 @@ namespace Microsoft.AspNetCore.Components.Test
public override async Task SetParametersAsync(ParameterView parameters)
{
lastParameterView = parameters;
NumSetParametersCalls++;
await base.SetParametersAsync(parameters);
}
@ -395,6 +432,13 @@ namespace Microsoft.AspNetCore.Components.Test
NumRenders++;
builder.AddContent(0, $"CascadingParameter={CascadingParameter}; RegularParameter={RegularParameter}");
}
public void AttemptIllegalAccessToLastParameterView()
{
// You're not allowed to hold onto a ParameterView and access it later,
// so this should throw
lastParameterView.TryGetValue<object>("anything", out _);
}
}
class SecondCascadingParameterConsumerComponent<T1, T2> : CascadingParameterConsumerComponent<T1>

View File

@ -673,7 +673,7 @@ namespace Microsoft.AspNetCore.Components
}
builder.CloseComponent();
var view = new ParameterView(builder.GetFrames().Array, ownerIndex: 0);
var view = new ParameterView(ParameterViewLifetime.Unbound, builder.GetFrames().Array, ownerIndex: 0);
var cascadingParameters = new List<CascadingParameterState>();
foreach (var kvp in _keyValuePairs)

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Components
{
RenderTreeFrame.ChildComponent(0, typeof(FakeComponent)).WithComponentSubtreeLength(1)
};
var parameters = new ParameterView(frames, 0);
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
// Assert
Assert.Empty(ToEnumerable(parameters));
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Components
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(1)
};
var parameters = new ParameterView(frames, 0);
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
// Assert
Assert.Empty(ToEnumerable(parameters));
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components
// end of the owner's descendants
RenderTreeFrame.Attribute(3, "orphaned attribute", "value")
};
var parameters = new ParameterView(frames, 0);
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
// Assert
Assert.Collection(ToEnumerable(parameters),
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Components
RenderTreeFrame.Element(3, "child element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(4, "child attribute", "some value")
};
var parameters = new ParameterView(frames, 0);
var parameters = new ParameterView(ParameterViewLifetime.Unbound, frames, 0);
// Assert
Assert.Collection(ToEnumerable(parameters),
@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Components
var attribute1Value = new object();
var attribute2Value = new object();
var attribute3Value = new object();
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value)
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Components
public void CanTryGetNonExistingValue()
{
// Arrange
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "some other entry", new object())
@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Components
public void CanTryGetExistingValueWithCorrectType()
{
// Arrange
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "my entry", "hello")
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var myEntryValue = new object();
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var myEntryValue = new object();
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
RenderTreeFrame.Attribute(1, "my entry", myEntryValue),
@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Components
public void CanGetValueOrDefault_WithNonExistingValue()
{
// Arrange
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "some other entry", new object())
@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var explicitDefaultValue = new DateTime(2018, 3, 20);
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "some other entry", new object())
@ -226,7 +226,7 @@ namespace Microsoft.AspNetCore.Components
public void ThrowsIfTryGetExistingValueWithIncorrectType()
{
// Arrange
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "my entry", "hello")
@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var entry2Value = new object();
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(3),
RenderTreeFrame.Attribute(0, "entry 1", "value 1"),
@ -304,7 +304,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var myEntryValue = new object();
var parameters = new ParameterView(new[]
var parameters = new ParameterView(ParameterViewLifetime.Unbound, new[]
{
RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
RenderTreeFrame.Attribute(1, "unrelated value", new object())
@ -322,6 +322,32 @@ namespace Microsoft.AspNetCore.Components
Assert.Same(myEntryValue, result);
}
[Fact]
public void CannotReadAfterLifetimeExpiry()
{
// Arrange
var builder = new RenderBatchBuilder();
var lifetime = new ParameterViewLifetime(builder);
var frames = new[]
{
RenderTreeFrame.ChildComponent(0, typeof(FakeComponent)).WithComponentSubtreeLength(1)
};
var parameterView = new ParameterView(lifetime, frames, 0);
// Act
builder.InvalidateParameterViews();
// Assert
Assert.Throws<InvalidOperationException>(() => parameterView.GetEnumerator());
Assert.Throws<InvalidOperationException>(() => parameterView.GetValueOrDefault<object>("anything"));
Assert.Throws<InvalidOperationException>(() => parameterView.SetParameterProperties(new object()));
Assert.Throws<InvalidOperationException>(() => parameterView.ToDictionary());
var ex = Assert.Throws<InvalidOperationException>(() => parameterView.TryGetValue<object>("anything", out _));
// It's enough to assert about one of the messages
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
}
private Action<ParameterValue> AssertParameter(string expectedName, object expectedValue, bool expectedIsCascading)
{
return parameter =>

View File

@ -3701,6 +3701,39 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Contains("Cannot start a batch when one is already in progress.", ex.Message);
}
[Fact]
public void CannotAccessParameterViewAfterSynchronousReturn()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = new TestComponent(builder =>
{
builder.OpenComponent<ParameterViewIllegalCapturingComponent>(0);
builder.AddAttribute(1, nameof(ParameterViewIllegalCapturingComponent.SomeParam), 0);
builder.CloseComponent();
});
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
// Note that we're not waiting for the async render to complete, since we want to assert
// about the situation immediately after the component yields the thread
renderer.RenderRootComponentAsync(rootComponentId);
// Act/Assert
var capturingComponent = (ParameterViewIllegalCapturingComponent)renderer.GetCurrentRenderTreeFrames(rootComponentId).Array[0].Component;
var parameterView = capturingComponent.CapturedParameterView;
// All public APIs on capturingComponent should be electrified now
// Internal APIs don't have to be, because we won't call them at the wrong time
Assert.Throws<InvalidOperationException>(() => parameterView.GetEnumerator());
Assert.Throws<InvalidOperationException>(() => parameterView.GetValueOrDefault<object>("anything"));
Assert.Throws<InvalidOperationException>(() => parameterView.SetParameterProperties(new object()));
Assert.Throws<InvalidOperationException>(() => parameterView.ToDictionary());
var ex = Assert.Throws<InvalidOperationException>(() => parameterView.TryGetValue<object>("anything", out _));
// It's enough to assert about one of the messages
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
}
private class NoOpRenderer : Renderer
{
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
@ -4443,5 +4476,25 @@ namespace Microsoft.AspNetCore.Components.Test
public new void ProcessPendingRender()
=> base.ProcessPendingRender();
}
class ParameterViewIllegalCapturingComponent : IComponent
{
public ParameterView CapturedParameterView { get; private set; }
[Parameter] public int SomeParam { get; set; }
public void Attach(RenderHandle renderHandle)
{
}
public Task SetParametersAsync(ParameterView parameters)
{
CapturedParameterView = parameters;
// Return a task that never completes to show that access is forbidden
// after the synchronous return, not just after the returned task completes
return new TaskCompletionSource<object>().Task;
}
}
}
}

View File

@ -111,8 +111,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var count = Descriptors.Count;
for (var i = 0; i < count; i++)
{
var (componentType, sequence) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, sequence.ToString());
var (componentType, parameters, sequence) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, parameters, sequence.ToString());
}
Log.InitializationSucceeded(_logger);

View File

@ -9,12 +9,11 @@ namespace Microsoft.AspNetCore.Components.Server
{
public Type ComponentType { get; set; }
public ParameterView Parameters { get; set; }
public int Sequence { get; set; }
public void Deconstruct(out Type componentType, out int sequence)
{
componentType = ComponentType;
sequence = Sequence;
}
public void Deconstruct(out Type componentType, out ParameterView parameters, out int sequence) =>
(componentType, sequence, parameters) = (ComponentType, Sequence, Parameters);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -70,12 +70,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
{
if (!_clientProxy.Connected)
if (_clientProxy is null)
{
throw new InvalidOperationException("JavaScript interop calls cannot be issued at this time. This is because the component is being " +
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not " +
"attempted during prerendering or while the client is disconnected.");
throw new InvalidOperationException(
"JavaScript interop calls cannot be issued at this time. This is because the component is being " +
$"statically rendererd. When prerendering is enabled, JavaScript interop calls can only be performed " +
$"during the OnAfterRenderAsync lifecycle method.");
}
Log.BeginInvokeJS(_logger, asyncHandle, identifier);

View File

@ -64,6 +64,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return RenderRootComponentAsync(componentId);
}
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="RemoteRenderer"/>,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="parameters">The parameters for the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public Task AddComponentAsync(Type componentType, ParameterView parameters, string domElementSelector)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
var attachComponentTask = _client.SendAsync("JS.AttachComponent", componentId, domElementSelector);
CaptureAsyncExceptions(attachComponentTask);
return RenderRootComponentAsync(componentId, parameters);
}
protected override void ProcessPendingRender()
{
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)

View File

@ -25,8 +25,12 @@ namespace Microsoft.AspNetCore.Components.Server
// 'sequence' indicates the order in which this component got rendered on the server.
// 'assemblyName' the assembly name for the rendered component.
// 'type' the full type name for the rendered component.
// 'parameterDefinitions' a JSON serialized array that contains the definitions for the parameters including their names and types and assemblies.
// 'parameterValues' a JSON serialized array containing the parameter values.
// 'invocationId' a random string that matches all components rendered by as part of a single HTTP response.
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>"}))
// With parameters
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>", parameterDefinitions: "[{ \"name\":\"Parameter\", \"typeName\":\"string\", \"assembly\":\"System.Private.CoreLib\"}], parameterValues: [<<string-value>>]}))
// Serialization:
// For a given response, MVC renders one or more markers in sequence, including a descriptor for each rendered
@ -55,11 +59,13 @@ namespace Microsoft.AspNetCore.Components.Server
private readonly IDataProtector _dataProtector;
private readonly ILogger<ServerComponentDeserializer> _logger;
private readonly ServerComponentTypeCache _rootComponentTypeCache;
private readonly ComponentParameterDeserializer _parametersDeserializer;
public ServerComponentDeserializer(
IDataProtectionProvider dataProtectionProvider,
ILogger<ServerComponentDeserializer> logger,
ServerComponentTypeCache rootComponentTypeCache)
ServerComponentTypeCache rootComponentTypeCache,
ComponentParameterDeserializer parametersDeserializer)
{
// When we protect the data we use a time-limited data protector with the
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
@ -74,6 +80,7 @@ namespace Microsoft.AspNetCore.Components.Server
_logger = logger;
_rootComponentTypeCache = rootComponentTypeCache;
_parametersDeserializer = parametersDeserializer;
}
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
@ -176,9 +183,16 @@ namespace Microsoft.AspNetCore.Components.Server
return default;
}
if (!_parametersDeserializer.TryDeserializeParameters(serverComponent.ParameterDefinitions, serverComponent.ParameterValues, out var parameters))
{
// TryDeserializeParameters does appropriate logging.
return default;
}
var componentDescriptor = new ComponentDescriptor
{
ComponentType = componentType,
Parameters = parameters,
Sequence = serverComponent.Sequence
};

View File

@ -58,6 +58,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<CircuitFactory>();
services.TryAddSingleton<ServerComponentDeserializer>();
services.TryAddSingleton<ServerComponentTypeCache>();
services.TryAddSingleton<ComponentParameterDeserializer>();
services.TryAddSingleton<ComponentParametersTypeCache>();
services.TryAddSingleton<CircuitIdFactory>();
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);

View File

@ -70,6 +70,7 @@
<!-- Shared descriptor infrastructure with MVC -->
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
</ItemGroup>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
@ -40,6 +41,45 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Assert.Equal(0, deserializedDescriptor.Sequence);
}
[Fact]
public void CanParseSingleMarkerWithParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(TestComponent), new Dictionary<string, object> { ["Parameter"] = "Value" })));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
var deserializedDescriptor = Assert.Single(descriptors);
Assert.Equal(typeof(TestComponent).FullName, deserializedDescriptor.ComponentType.FullName);
Assert.Equal(0, deserializedDescriptor.Sequence);
var parameters = deserializedDescriptor.Parameters.ToDictionary();
Assert.Single(parameters);
Assert.Contains("Parameter", parameters.Keys);
Assert.Equal("Value", parameters["Parameter"]);
}
[Fact]
public void CanParseSingleMarkerWithNullParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(TestComponent), new Dictionary<string, object> { ["Parameter"] = null })));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
var deserializedDescriptor = Assert.Single(descriptors);
Assert.Equal(typeof(TestComponent).FullName, deserializedDescriptor.ComponentType.FullName);
Assert.Equal(0, deserializedDescriptor.Sequence);
var parameters = deserializedDescriptor.Parameters.ToDictionary();
Assert.Single(parameters);
Assert.Contains("Parameter", parameters.Keys);
Assert.Null(parameters["Parameter"]);
}
[Fact]
public void CanParseMultipleMarkers()
{
@ -60,6 +100,65 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Assert.Equal(1, secondDescriptor.Sequence);
}
[Fact]
public void CanParseMultipleMarkersWithParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(TestComponent), new Dictionary<string, object> { ["First"] = "Value" }),
(typeof(TestComponent), new Dictionary<string, object> { ["Second"] = null })));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Equal(2, descriptors.Count);
var firstDescriptor = descriptors[0];
Assert.Equal(typeof(TestComponent).FullName, firstDescriptor.ComponentType.FullName);
Assert.Equal(0, firstDescriptor.Sequence);
var firstParameters = firstDescriptor.Parameters.ToDictionary();
Assert.Single(firstParameters);
Assert.Contains("First", firstParameters.Keys);
Assert.Equal("Value", firstParameters["First"]);
var secondDescriptor = descriptors[1];
Assert.Equal(typeof(TestComponent).FullName, secondDescriptor.ComponentType.FullName);
Assert.Equal(1, secondDescriptor.Sequence);
var secondParameters = secondDescriptor.Parameters.ToDictionary();
Assert.Single(secondParameters);
Assert.Contains("Second", secondParameters.Keys);
Assert.Null(secondParameters["Second"]);
}
[Fact]
public void CanParseMultipleMarkersWithAndWithoutParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(TestComponent), new Dictionary<string, object> { ["First"] = "Value" }),
(typeof(TestComponent), null)));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Equal(2, descriptors.Count);
var firstDescriptor = descriptors[0];
Assert.Equal(typeof(TestComponent).FullName, firstDescriptor.ComponentType.FullName);
Assert.Equal(0, firstDescriptor.Sequence);
var firstParameters = firstDescriptor.Parameters.ToDictionary();
Assert.Single(firstParameters);
Assert.Contains("First", firstParameters.Keys);
Assert.Equal("Value", firstParameters["First"]);
var secondDescriptor = descriptors[1];
Assert.Equal(typeof(TestComponent).FullName, secondDescriptor.ComponentType.FullName);
Assert.Equal(1, secondDescriptor.Sequence);
Assert.Empty(secondDescriptor.Parameters.ToDictionary());
}
[Fact]
public void DoesNotParseOutOfOrderMarkers()
{
@ -213,7 +312,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private string SerializeComponent(string assembly, string type) =>
JsonSerializer.Serialize(
new ServerComponent(0, assembly, type, Guid.NewGuid()),
new ServerComponent(0, assembly, type, Array.Empty<ComponentParameter>(), Array.Empty<object>(), Guid.NewGuid()),
ServerComponentSerializationSettings.JsonSerializationOptions);
private ServerComponentDeserializer CreateServerComponentDeserializer()
@ -221,7 +320,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return new ServerComponentDeserializer(
_ephemeralDataProtectionProvider,
NullLogger<ServerComponentDeserializer>.Instance,
new ServerComponentTypeCache());
new ServerComponentTypeCache(),
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
}
private string SerializeMarkers(ServerComponentMarker[] markers) =>
@ -233,7 +333,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var markers = new ServerComponentMarker[types.Length];
for (var i = 0; i < types.Length; i++)
{
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], false);
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], ParameterView.Empty, false);
}
return markers;
}
private ServerComponentMarker[] CreateMarkers(params (Type, Dictionary<string,object>)[] types)
{
var serializer = new ServerComponentSerializer(_ephemeralDataProtectionProvider);
var markers = new ServerComponentMarker[types.Length];
for (var i = 0; i < types.Length; i++)
{
var (type, parameters) = types[i];
markers[i] = serializer.SerializeInvocation(
_invocationSequence,
type,
parameters == null ? ParameterView.Empty : ParameterView.FromDictionary(parameters),
false);
}
return markers;
@ -245,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var markers = new ServerComponentMarker[types.Length];
for (var i = 0; i < types.Length; i++)
{
markers[i] = serializer.SerializeInvocation(sequence, types[i], false);
markers[i] = serializer.SerializeInvocation(sequence, types[i], ParameterView.Empty, false);
}
return markers;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -25,10 +25,14 @@ export const monoPlatform: Platform = {
window['Browser'] = {
init: () => { },
};
// Emscripten works by expecting the module config to be a global
window['Module'] = createEmscriptenModuleInstance(loadAssemblyUrls, resolve, reject);
addScriptTagsToDocument();
// Emscripten works by expecting the module config to be a global
// For compatibility with macOS Catalina, we have to assign a temporary value to window.Module
// before we start loading the WebAssembly files
addGlobalModuleScriptTagsToDocument(() => {
window['Module'] = createEmscriptenModuleInstance(loadAssemblyUrls, resolve, reject);
addScriptTagsToDocument();
});
});
},
@ -205,6 +209,23 @@ function addScriptTagsToDocument() {
document.body.appendChild(scriptElem);
}
// Due to a strange behavior in macOS Catalina, we have to delay loading the WebAssembly files
// until after it finishes evaluating a <script> element that assigns a value to window.Module.
// This may be fixed in a later version of macOS/iOS, or even if not it may be possible to reduce
// this to a smaller workaround.
function addGlobalModuleScriptTagsToDocument(callback: () => void) {
const scriptElem = document.createElement('script');
// This pollutes global but is needed so it can be called from the script.
// The callback is put in the global scope so that it can be run after the script is loaded.
// onload cannot be used in this case for non-file scripts.
window['__wasmmodulecallback__'] = callback;
scriptElem.type = 'text/javascript';
scriptElem.text = 'var Module; window.__wasmmodulecallback__(); delete window.__wasmmodulecallback__;';
document.body.appendChild(scriptElem);
}
function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () => void, onError: (reason?: any) => void) {
const module = {} as typeof Module;
const wasmBinaryFile = '_framework/wasm/mono.wasm';

View File

@ -5,11 +5,15 @@ import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalCh
import { applyCaptureIdToElement } from './ElementReferenceCapture';
import { EventFieldInfo } from './EventFieldInfo';
import { dispatchEvent } from './RendererEventDispatcher';
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
const selectValuePropname = '_blazorSelectValue';
const sharedTemplateElemForParsing = document.createElement('template');
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {};
const internalAttributeNamePrefix = '__internal_';
const eventPreventDefaultAttributeNamePrefix = 'preventDefault_';
const eventStopPropagationAttributeNamePrefix = 'stopPropagation_';
export class BrowserRenderer {
private eventDelegator: EventDelegator;
@ -23,6 +27,11 @@ export class BrowserRenderer {
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => {
raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs, eventFieldInfo);
});
// We don't yet know whether or not navigation interception will be enabled, but in case it will be,
// we wire up the navigation manager to the event delegator so it has the option to participate
// in the synthetic event bubbling process later
attachNavigationManagerToEventDelegator(this.eventDelegator);
}
public attachRootComponentToLogicalElement(componentId: number, element: LogicalElement): void {
@ -281,15 +290,10 @@ export class BrowserRenderer {
private applyAttribute(batch: RenderBatch, componentId: number, toDomElement: Element, attributeFrame: RenderTreeFrame) {
const frameReader = batch.frameReader;
const attributeName = frameReader.attributeName(attributeFrame)!;
const browserRendererId = this.browserRendererId;
const eventHandlerId = frameReader.attributeEventHandlerId(attributeFrame);
if (eventHandlerId) {
const firstTwoChars = attributeName.substring(0, 2);
const eventName = attributeName.substring(2);
if (firstTwoChars !== 'on' || !eventName) {
throw new Error(`Attribute has nonzero event handler ID, but attribute name '${attributeName}' does not start with 'on'.`);
}
const eventName = stripOnPrefix(attributeName);
this.eventDelegator.setListener(toDomElement, eventName, eventHandlerId, componentId);
return;
}
@ -310,8 +314,30 @@ export class BrowserRenderer {
return this.tryApplyValueProperty(batch, element, attributeFrame);
case 'checked':
return this.tryApplyCheckedProperty(batch, element, attributeFrame);
default:
default: {
if (attributeName.startsWith(internalAttributeNamePrefix)) {
this.applyInternalAttribute(batch, element, attributeName.substring(internalAttributeNamePrefix.length), attributeFrame);
return true;
}
return false;
}
}
}
private applyInternalAttribute(batch: RenderBatch, element: Element, internalAttributeName: string, attributeFrame: RenderTreeFrame | null) {
const attributeValue = attributeFrame ? batch.frameReader.attributeValue(attributeFrame) : null;
if (internalAttributeName.startsWith(eventStopPropagationAttributeNamePrefix)) {
// Stop propagation
const eventName = stripOnPrefix(internalAttributeName.substring(eventStopPropagationAttributeNamePrefix.length));
this.eventDelegator.setStopPropagation(element, eventName, attributeValue !== null);
} else if (internalAttributeName.startsWith(eventPreventDefaultAttributeNamePrefix)) {
// Prevent default
const eventName = stripOnPrefix(internalAttributeName.substring(eventPreventDefaultAttributeNamePrefix.length));
this.eventDelegator.setPreventDefault(element, eventName, attributeValue !== null);
} else {
// The prefix makes this attribute name reserved, so any other usage is disallowed
throw new Error(`Unsupported internal attribute '${internalAttributeName}'`);
}
}
@ -477,3 +503,11 @@ function clearBetween(start: Node, end: Node): void {
// as it adds noise to the DOM.
start.textContent = '!';
}
function stripOnPrefix(attributeName: string) {
if (attributeName.startsWith('on')) {
return attributeName.substring(2);
}
throw new Error(`Attribute should be an event name, but doesn't start with 'on'. Value: '${attributeName}'`);
}

View File

@ -31,7 +31,9 @@ export interface OnEventCallback {
export class EventDelegator {
private static nextEventDelegatorId = 0;
private eventsCollectionKey: string;
private readonly eventsCollectionKey: string;
private readonly afterClickCallbacks: ((event: MouseEvent) => void)[] = [];
private eventInfoStore: EventInfoStore;
@ -42,21 +44,18 @@ export class EventDelegator {
}
public setListener(element: Element, eventName: string, eventHandlerId: number, renderingComponentId: number) {
// Ensure we have a place to store event info for this element
let infoForElement: EventHandlerInfosForElement = element[this.eventsCollectionKey];
if (!infoForElement) {
infoForElement = element[this.eventsCollectionKey] = {};
}
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
const existingHandler = infoForElement.getHandler(eventName);
if (infoForElement.hasOwnProperty(eventName)) {
if (existingHandler) {
// We can cheaply update the info on the existing object and don't need any other housekeeping
const oldInfo = infoForElement[eventName];
this.eventInfoStore.update(oldInfo.eventHandlerId, eventHandlerId);
// Note that this also takes care of updating the eventHandlerId on the existing handler object
this.eventInfoStore.update(existingHandler.eventHandlerId, eventHandlerId);
} else {
// Go through the whole flow which might involve registering a new global handler
const newInfo = { element, eventName, eventHandlerId, renderingComponentId };
this.eventInfoStore.add(newInfo);
infoForElement[eventName] = newInfo;
infoForElement.setHandler(eventName, newInfo);
}
}
@ -69,16 +68,31 @@ export class EventDelegator {
// Looks like this event handler wasn't already disposed
// Remove the associated data from the DOM element
const element = info.element;
if (element.hasOwnProperty(this.eventsCollectionKey)) {
const elementEventInfos: EventHandlerInfosForElement = element[this.eventsCollectionKey];
delete elementEventInfos[info.eventName];
if (Object.getOwnPropertyNames(elementEventInfos).length === 0) {
delete element[this.eventsCollectionKey];
}
const elementEventInfos = this.getEventHandlerInfosForElement(element, false);
if (elementEventInfos) {
elementEventInfos.removeHandler(info.eventName);
}
}
}
public notifyAfterClick(callback: (event: MouseEvent) => void) {
// This is extremely special-case. It's needed so that navigation link click interception
// can be sure to run *after* our synthetic bubbling process. If a need arises, we can
// generalise this, but right now it's a purely internal detail.
this.afterClickCallbacks.push(callback);
this.eventInfoStore.addGlobalListener('click'); // Ensure we always listen for this
}
public setStopPropagation(element: Element, eventName: string, value: boolean) {
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
infoForElement.stopPropagation(eventName, value);
}
public setPreventDefault(element: Element, eventName: string, value: boolean) {
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
infoForElement.preventDefault(eventName, value);
}
private onGlobalEvent(evt: Event) {
if (!(evt.target instanceof Element)) {
return;
@ -88,22 +102,46 @@ export class EventDelegator {
let candidateElement = evt.target as Element | null;
let eventArgs: EventForDotNet<UIEventArgs> | null = null; // Populate lazily
const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
let stopPropagationWasRequested = false;
while (candidateElement) {
if (candidateElement.hasOwnProperty(this.eventsCollectionKey)) {
const handlerInfos: EventHandlerInfosForElement = candidateElement[this.eventsCollectionKey];
if (handlerInfos.hasOwnProperty(evt.type)) {
const handlerInfos = this.getEventHandlerInfosForElement(candidateElement, false);
if (handlerInfos) {
const handlerInfo = handlerInfos.getHandler(evt.type);
if (handlerInfo) {
// We are going to raise an event for this element, so prepare info needed by the .NET code
if (!eventArgs) {
eventArgs = EventForDotNet.fromDOMEvent(evt);
}
const handlerInfo = handlerInfos[evt.type];
const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs, eventFieldInfo);
}
if (handlerInfos.stopPropagation(evt.type)) {
stopPropagationWasRequested = true;
}
if (handlerInfos.preventDefault(evt.type)) {
evt.preventDefault();
}
}
candidateElement = eventIsNonBubbling ? null : candidateElement.parentElement;
candidateElement = (eventIsNonBubbling || stopPropagationWasRequested) ? null : candidateElement.parentElement;
}
// Special case for navigation interception
if (evt.type === 'click') {
this.afterClickCallbacks.forEach(callback => callback(evt as MouseEvent));
}
}
private getEventHandlerInfosForElement(element: Element, createIfNeeded: boolean): EventHandlerInfosForElement | null {
if (element.hasOwnProperty(this.eventsCollectionKey)) {
return element[this.eventsCollectionKey];
} else if (createIfNeeded) {
return (element[this.eventsCollectionKey] = new EventHandlerInfosForElement());
} else {
return null;
}
}
}
@ -126,7 +164,10 @@ class EventInfoStore {
this.infosByEventHandlerId[info.eventHandlerId] = info;
const eventName = info.eventName;
this.addGlobalListener(info.eventName);
}
public addGlobalListener(eventName: string) {
if (this.countByEventName.hasOwnProperty(eventName)) {
this.countByEventName[eventName]++;
} else {
@ -168,14 +209,46 @@ class EventInfoStore {
}
}
interface EventHandlerInfosForElement {
class EventHandlerInfosForElement {
// Although we *could* track multiple event handlers per (element, eventName) pair
// (since they have distinct eventHandlerId values), there's no point doing so because
// our programming model is that you declare event handlers as attributes. An element
// can only have one attribute with a given name, hence only one event handler with
// that name at any one time.
// So to keep things simple, only track one EventHandlerInfo per (element, eventName)
[eventName: string]: EventHandlerInfo;
private handlers: { [eventName: string]: EventHandlerInfo } = {};
private preventDefaultFlags: { [eventName: string]: boolean } | null = null;
private stopPropagationFlags: { [eventName: string]: boolean } | null = null;
public getHandler(eventName: string): EventHandlerInfo | null {
return this.handlers.hasOwnProperty(eventName) ? this.handlers[eventName] : null;
}
public setHandler(eventName: string, handler: EventHandlerInfo) {
this.handlers[eventName] = handler;
}
public removeHandler(eventName: string) {
delete this.handlers[eventName];
}
public preventDefault(eventName: string, setValue?: boolean): boolean {
if (setValue !== undefined) {
this.preventDefaultFlags = this.preventDefaultFlags || {};
this.preventDefaultFlags[eventName] = setValue;
}
return this.preventDefaultFlags ? this.preventDefaultFlags[eventName] : false;
}
public stopPropagation(eventName: string, setValue?: boolean): boolean {
if (setValue !== undefined) {
this.stopPropagationFlags = this.stopPropagationFlags || {};
this.stopPropagationFlags[eventName] = setValue;
}
return this.stopPropagationFlags ? this.stopPropagationFlags[eventName] : false;
}
}
interface EventHandlerInfo {

View File

@ -1,7 +1,8 @@
import '@dotnet/jsinterop';
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
import { EventDelegator } from '../Rendering/EventDelegator';
let hasRegisteredNavigationInterception = false;
let hasEnabledNavigationInterception = false;
let hasRegisteredNavigationEventListeners = false;
// Will be initialized once someone registers
@ -28,21 +29,30 @@ function listenForNavigationEvents(callback: (uri: string, intercepted: boolean)
}
function enableNavigationInterception() {
if (hasRegisteredNavigationInterception) {
return;
}
hasEnabledNavigationInterception = true;
}
hasRegisteredNavigationInterception = true;
export function attachToEventDelegator(eventDelegator: EventDelegator) {
// We need to respond to clicks on <a> elements *after* the EventDelegator has finished
// running its simulated bubbling process so that we can respect any preventDefault requests.
// So instead of registering our own native event, register using the EventDelegator.
eventDelegator.notifyAfterClick(event => {
if (!hasEnabledNavigationInterception) {
return;
}
document.addEventListener('click', event => {
if (event.button !== 0 || eventHasSpecialKey(event)) {
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
return;
}
if (event.defaultPrevented) {
return;
}
// Intercept clicks on all <a> elements where the href is within the <base href> URI space
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement;
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement | null;
const hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
const targetAttributeValue = anchorTarget.getAttribute('target');

View File

@ -409,6 +409,11 @@ namespace Microsoft.AspNetCore.Components.Web
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Web.TouchEventArgs> Create(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Func<Microsoft.AspNetCore.Components.Web.TouchEventArgs, System.Threading.Tasks.Task> callback) { throw null; }
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Web.WheelEventArgs> Create(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Func<Microsoft.AspNetCore.Components.Web.WheelEventArgs, System.Threading.Tasks.Task> callback) { throw null; }
}
public static partial class WebRenderTreeBuilderExtensions
{
public static void AddEventPreventDefaultAttribute(this Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int sequence, string eventName, bool value) { }
public static void AddEventStopPropagationAttribute(this Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int sequence, string eventName, bool value) { }
}
public partial class WheelEventArgs : Microsoft.AspNetCore.Components.Web.MouseEventArgs
{
public WheelEventArgs() { }

View File

@ -409,6 +409,11 @@ namespace Microsoft.AspNetCore.Components.Web
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Web.TouchEventArgs> Create(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Func<Microsoft.AspNetCore.Components.Web.TouchEventArgs, System.Threading.Tasks.Task> callback) { throw null; }
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Web.WheelEventArgs> Create(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Func<Microsoft.AspNetCore.Components.Web.WheelEventArgs, System.Threading.Tasks.Task> callback) { throw null; }
}
public static partial class WebRenderTreeBuilderExtensions
{
public static void AddEventPreventDefaultAttribute(this Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int sequence, string eventName, bool value) { }
public static void AddEventStopPropagationAttribute(this Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder, int sequence, string eventName, bool value) { }
}
public partial class WheelEventArgs : Microsoft.AspNetCore.Components.Web.MouseEventArgs
{
public WheelEventArgs() { }

View File

@ -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);
}
}
}

View File

@ -52,6 +52,7 @@
<ItemGroup>
<!-- Shared descriptor infrastructure with MVC -->
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
</ItemGroup>

View File

@ -68,9 +68,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var greets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
Assert.Equal(4, greets.Length); // 1 statically rendered + 3 prerendered
Assert.Equal(5, greets.Length); // 1 statically rendered + 3 prerendered + 1 server prerendered
Assert.Single(greets, "Hello John");
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 prerendered
Assert.Single(greets, "Hello Abraham");
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 server prerendered without parameters
var content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
var markers = ReadMarkers(content);
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
@ -84,6 +85,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
true,
false,
true,
false,
true
};
Assert.Equal(expectedComponentSequence, componentSequence);
@ -93,6 +96,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Browser.Exists(By.CssSelector("h3.interactive"));
var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
Assert.Equal(7, updatedGreets.Where(g => string.Equals("Hello Alfred", g)).Count());
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Albert", g)));
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Abraham", g)));
}
private (ServerComponentMarker, ServerComponentMarker)[] ReadMarkers(string content)

View File

@ -99,12 +99,102 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Empty(GetLogLines);
}
[Theory]
[InlineData("target")]
[InlineData("intermediate")]
public void StopPropagation(string whereToStopPropagation)
{
// If stopPropagation is off, we observe the event on the listener and all its ancestors
Browser.FindElement(By.Id("button-with-onclick")).Click();
Browser.Equal(new[] { "target onclick", "parent onclick" }, GetLogLines);
// If stopPropagation is on, the event doesn't reach the ancestor
// Note that in the "intermediate element" case, the intermediate element does *not* itself
// listen for this event, which shows that stopPropagation works independently of handling
ClearLog();
Browser.FindElement(By.Id($"{whereToStopPropagation}-stop-propagation")).Click();
Browser.FindElement(By.Id("button-with-onclick")).Click();
Browser.Equal(new[] { "target onclick" }, GetLogLines);
// We can also toggle it back off
ClearLog();
Browser.FindElement(By.Id($"{whereToStopPropagation}-stop-propagation")).Click();
Browser.FindElement(By.Id("button-with-onclick")).Click();
Browser.Equal(new[] { "target onclick", "parent onclick" }, GetLogLines);
}
[Fact]
public void PreventDefaultWorksOnTarget()
{
// Clicking a checkbox without preventDefault produces both "click" and "change"
// events, and it becomes checked
var checkboxWithoutPreventDefault = Browser.FindElement(By.Id("checkbox-with-preventDefault-false"));
checkboxWithoutPreventDefault.Click();
Browser.Equal(new[] { "Checkbox click", "Checkbox change" }, GetLogLines);
Browser.True(() => checkboxWithoutPreventDefault.Selected);
// Clicking a checkbox with preventDefault produces a "click" event, but no "change"
// event, and it remains unchecked
ClearLog();
var checkboxWithPreventDefault = Browser.FindElement(By.Id("checkbox-with-preventDefault-true"));
checkboxWithPreventDefault.Click();
Browser.Equal(new[] { "Checkbox click" }, GetLogLines);
Browser.False(() => checkboxWithPreventDefault.Selected);
}
[Fact]
public void PreventDefault_WorksOnAncestorElement()
{
// Even though the checkbox we're clicking this case does *not* have preventDefault,
// if its ancestor does, then we don't get the "change" event and it remains unchecked
Browser.FindElement(By.Id($"ancestor-prevent-default")).Click();
var checkboxWithoutPreventDefault = Browser.FindElement(By.Id("checkbox-with-preventDefault-false"));
checkboxWithoutPreventDefault.Click();
Browser.Equal(new[] { "Checkbox click" }, GetLogLines);
Browser.False(() => checkboxWithoutPreventDefault.Selected);
// We can also toggle it back off dynamically
Browser.FindElement(By.Id($"ancestor-prevent-default")).Click();
ClearLog();
checkboxWithoutPreventDefault.Click();
Browser.Equal(new[] { "Checkbox click", "Checkbox change" }, GetLogLines);
Browser.True(() => checkboxWithoutPreventDefault.Selected);
}
[Fact]
public void PreventDefaultCanBlockKeystrokes()
{
// By default, the textbox accepts keystrokes
var textbox = Browser.FindElement(By.Id($"textbox-that-can-block-keystrokes"));
textbox.SendKeys("a");
Browser.Equal(new[] { "Received keydown" }, GetLogLines);
Browser.Equal("a", () => textbox.GetAttribute("value"));
// We can turn on preventDefault to stop keystrokes
// There will still be a keydown event, but we're preventing it from actually changing the textbox value
ClearLog();
Browser.FindElement(By.Id($"prevent-keydown")).Click();
textbox.SendKeys("b");
Browser.Equal(new[] { "Received keydown" }, GetLogLines);
Browser.Equal("a", () => textbox.GetAttribute("value"));
// We can turn it back off
ClearLog();
Browser.FindElement(By.Id($"prevent-keydown")).Click();
textbox.SendKeys("c");
Browser.Equal(new[] { "Received keydown" }, GetLogLines);
Browser.Equal("ac", () => textbox.GetAttribute("value"));
}
private string[] GetLogLines()
=> Browser.FindElement(By.TagName("textarea"))
.GetAttribute("value")
.Replace("\r\n", "\n")
.Split('\n', StringSplitOptions.RemoveEmptyEntries);
void ClearLog()
=> Browser.FindElement(By.Id("clear-log")).Click();
private void TriggerCustomBubblingEvent(string elementId, string eventName)
{
var jsExecutor = (IJavaScriptExecutor)Browser;

View File

@ -455,6 +455,49 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal(0, () => BrowserScrollY);
}
[Theory]
[InlineData("external", "ancestor")]
[InlineData("external", "target")]
[InlineData("external", "descendant")]
[InlineData("internal", "ancestor")]
[InlineData("internal", "target")]
[InlineData("internal", "descendant")]
public void PreventDefault_CanBlockNavigation(string navigationType, string whereToPreventDefault)
{
SetUrlViaPushState("/PreventDefaultCases");
var app = Browser.MountTestComponent<TestRouter>();
var preventDefaultToggle = app.FindElement(By.CssSelector($".prevent-default .{whereToPreventDefault}"));
var linkElement = app.FindElement(By.Id($"{navigationType}-navigation"));
var counterButton = app.FindElement(By.ClassName("counter-button"));
if (whereToPreventDefault == "descendant")
{
// We're testing clicks on the link's descendant element
linkElement = linkElement.FindElement(By.TagName("span"));
}
// If preventDefault is on, then navigation does not occur
preventDefaultToggle.Click();
linkElement.Click();
// We check that no navigation ocurred by observing that we can still use the counter
counterButton.Click();
Browser.Equal("Counter: 1", () => counterButton.Text);
// Now if we toggle preventDefault back off, then navigation will occur
preventDefaultToggle.Click();
linkElement.Click();
if (navigationType == "external")
{
Browser.Equal("about:blank", () => Browser.Url);
}
else
{
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
}
}
private long BrowserScrollY
{
get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY");

View File

@ -1,15 +1,67 @@
<h3 id="event-bubbling">Bubbling standard event</h3>
@* Temporarily hard-coding the internal names - this will be replaced once the Razor compiler supports @onevent:stopPropagation and @onevent:preventDefault *@
<div @onclick="@(() => LogEvent("parent onclick"))">
<button id="button-with-onclick" @onclick="@(() => LogEvent("target onclick"))">Button with onclick handler</button>
<button id="button-without-onclick" >Button without onclick handler</button>
@* This element shows you can stop propagation even without necessarily also handling the event *@
<div __internal_stopPropagation_onclick="@intermediateStopPropagation">
<button id="button-with-onclick" @onclick="@(() => LogEvent("target onclick"))" __internal_stopPropagation_onclick="@targetStopPropagation">
Button with onclick handler
</button>
<button id="button-without-onclick">
Button without onclick handler
</button>
</div>
</div>
<fieldset>
<legend>Options</legend>
<label>
<input id="intermediate-stop-propagation" type="checkbox" @bind="intermediateStopPropagation" />
Stop propagation on intermediate element
</label>
<label>
<input id="target-stop-propagation" type="checkbox" @bind="targetStopPropagation" />
Stop propagation on target element
</label>
</fieldset>
<h3>PreventDefault</h3>
<div __internal_preventDefault_onclick="@ancestorPreventDefault">
<p>
<label>
<input type="checkbox" id="checkbox-with-preventDefault-true" __internal_preventDefault_onclick @onclick="@(() => LogEvent("Checkbox click"))" @onchange="@(() => LogEvent("Checkbox change"))" />
Checkbox with onclick preventDefault
</label>
</p>
<p>
<label>
<input type="checkbox" id="checkbox-with-preventDefault-false" __internal_preventDefault_onclick="@false" @onclick="@(() => LogEvent("Checkbox click"))" @onchange="@(() => LogEvent("Checkbox change"))" />
Checkbox with onclick preventDefault = false
</label>
</p>
<p>
Textbox that can block keystrokes: <input id="textbox-that-can-block-keystrokes" __internal_preventDefault_onkeydown="@preventOnKeyDown" @onkeydown="@(() => LogEvent("Received keydown"))" />
</p>
</div>
<fieldset>
<legend>Options</legend>
<label>
<input id="ancestor-prevent-default" type="checkbox" @bind="ancestorPreventDefault" />
Prevent default on ancestor
</label>
<label>
<input id="prevent-keydown" type="checkbox" @bind="preventOnKeyDown" />
Block keystrokes
</label>
</fieldset>
<h3>Bubbling custom event</h3>
<div onsneeze="@(new Action(() => LogEvent("parent onsneeze")))">
<div id="element-with-onsneeze" onsneeze="@(new Action(() => LogEvent("target onsneeze")))">Element with onsneeze handler</div>
<div id="element-without-onsneeze" >Element without onsneeze handler</div>
<div id="element-without-onsneeze">Element without onsneeze handler</div>
</div>
<h3>Non-bubbling standard event</h3>
@ -23,8 +75,13 @@
<h3>Event log</h3>
<textarea readonly @bind="logValue"></textarea>
<button id="clear-log" @onclick="@(() => { logValue = string.Empty; })">Clear log</button>
@code {
bool intermediateStopPropagation;
bool targetStopPropagation;
bool ancestorPreventDefault;
bool preventOnKeyDown;
string logValue = string.Empty;
void LogEvent(string message)

View File

@ -9,7 +9,7 @@
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
if (firstRender && Name == null)
{
Name = "Alfred";
interactive = "interactive";

View File

@ -20,6 +20,7 @@
<li><NavLink href="/subdir/WithParameters/Name/Abc/LastName/McDef">With more parameters</NavLink></li>
<li><NavLink href="/subdir/LongPage1">Long page 1</NavLink></li>
<li><NavLink href="/subdir/LongPage2">Long page 2</NavLink></li>
<li><NavLink href="PreventDefaultCases">preventDefault cases</NavLink></li>
<li><NavLink>Null href never matches</NavLink></li>
</ul>

View File

@ -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 &lt;a&gt; 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;
}

View File

@ -21,7 +21,16 @@ namespace TestServer
services.AddMvc();
services.AddCors(options =>
{
options.AddPolicy("AllowAll", _ => { /* Controlled below */ });
// It's not enough just to return "Access-Control-Allow-Origin: *", because
// browsers don't allow wildcards in conjunction with credentials. So we must
// specify explicitly which origin we want to allow.
options.AddPolicy("AllowAll", policy => policy
.SetIsOriginAllowed(host => host.StartsWith("http://localhost:") || host.StartsWith("http://127.0.0.1:"))
.AllowAnyHeader()
.WithExposedHeaders("MyCustomHeader")
.AllowAnyMethod()
.AllowCredentials());
});
}
@ -33,18 +42,6 @@ namespace TestServer
app.UseDeveloperExceptionPage();
}
// It's not enough just to return "Access-Control-Allow-Origin: *", because
// browsers don't allow wildcards in conjunction with credentials. So we must
// specify explicitly which origin we want to allow.
app.UseCors(policy =>
{
policy.SetIsOriginAllowed(host => host.StartsWith("http://localhost:") || host.StartsWith("http://127.0.0.1:"))
.AllowAnyHeader()
.WithExposedHeaders("MyCustomHeader")
.AllowAnyMethod()
.AllowCredentials();
});
// Mount the server-side Blazor app on /subdir
app.Map("/subdir", app =>
{
@ -52,12 +49,14 @@ namespace TestServer
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
app.UseRouting();
app.UseCors();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
});
});
}
}

View File

@ -27,6 +27,10 @@
<p>Some content after</p>
</div>
</div>
<div id="container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" }))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" }))
</div>
</div>
@*

View File

@ -1,6 +1,7 @@
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -68,9 +69,7 @@ namespace TestServer
</html>");
var content = writer.ToString();
ctx.Response.ContentLength = content.Length;
using var responseWriter = new StreamWriter(ctx.Response.Body);
await responseWriter.WriteAsync(content);
await responseWriter.FlushAsync();
await ctx.Response.WriteAsync(content);
});
}
}

View File

@ -316,6 +316,7 @@ namespace Microsoft.Net.Http.Headers
}
public enum SameSiteMode
{
Unspecified = -1,
None = 0,
Lax = 1,
Strict = 2,

View File

@ -5,12 +5,14 @@ namespace Microsoft.Net.Http.Headers
{
/// <summary>
/// Indicates if the client should include a cookie on "same-site" or "cross-site" requests.
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
/// </summary>
// This mirrors Microsoft.AspNetCore.Http.SameSiteMode
public enum SameSiteMode
{
/// <summary>No SameSite field will be set, the client should follow its default cookie policy.</summary>
Unspecified = -1,
/// <summary>Indicates the client should disable same-site restrictions.</summary>
None = 0,
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
Lax,

View File

@ -20,8 +20,14 @@ namespace Microsoft.Net.Http.Headers
private const string SecureToken = "secure";
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
private const string SameSiteToken = "samesite";
private static readonly string SameSiteNoneToken = SameSiteMode.None.ToString().ToLower();
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
internal static bool SuppressSameSiteNone;
private const string HttpOnlyToken = "httponly";
private const string SeparatorToken = "; ";
private const string EqualsToken = "=";
@ -36,6 +42,14 @@ namespace Microsoft.Net.Http.Headers
private StringSegment _name;
private StringSegment _value;
static SetCookieHeaderValue()
{
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
{
SuppressSameSiteNone = enabled;
}
}
private SetCookieHeaderValue()
{
// Used by the parser to create a new instance of this type.
@ -92,16 +106,17 @@ namespace Microsoft.Net.Http.Headers
public bool Secure { get; set; }
public SameSiteMode SameSite { get; set; }
public SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
public bool HttpOnly { get; set; }
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly
public override string ToString()
{
var length = _name.Length + EqualsToken.Length + _value.Length;
string maxAge = null;
string sameSite = null;
if (Expires.HasValue)
{
@ -129,9 +144,20 @@ namespace Microsoft.Net.Http.Headers
length += SeparatorToken.Length + SecureToken.Length;
}
if (SameSite != SameSiteMode.None)
// Allow for Unspecified (-1) to skip SameSite
if (SameSite == SameSiteMode.None && !SuppressSameSiteNone)
{
var sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
sameSite = SameSiteNoneToken;
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
}
else if (SameSite == SameSiteMode.Lax)
{
sameSite = SameSiteLaxToken;
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
}
else if (SameSite == SameSiteMode.Strict)
{
sameSite = SameSiteStrictToken;
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
}
@ -140,9 +166,9 @@ namespace Microsoft.Net.Http.Headers
length += SeparatorToken.Length + HttpOnlyToken.Length;
}
return string.Create(length, (this, maxAge), (span, tuple) =>
return string.Create(length, (this, maxAge, sameSite), (span, tuple) =>
{
var (headerValue, maxAgeValue) = tuple;
var (headerValue, maxAgeValue, sameSite) = tuple;
Append(ref span, headerValue._name);
Append(ref span, EqualsToken);
@ -180,9 +206,9 @@ namespace Microsoft.Net.Http.Headers
AppendSegment(ref span, SecureToken, null);
}
if (headerValue.SameSite != SameSiteMode.None)
if (sameSite != null)
{
AppendSegment(ref span, SameSiteToken, headerValue.SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
AppendSegment(ref span, SameSiteToken, sameSite);
}
if (headerValue.HttpOnly)
@ -248,9 +274,18 @@ namespace Microsoft.Net.Http.Headers
AppendSegment(builder, SecureToken, null);
}
if (SameSite != SameSiteMode.None)
// Allow for Unspecified (-1) to skip SameSite
if (SameSite == SameSiteMode.None && !SuppressSameSiteNone)
{
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
AppendSegment(builder, SameSiteToken, SameSiteNoneToken);
}
else if (SameSite == SameSiteMode.Lax)
{
AppendSegment(builder, SameSiteToken, SameSiteLaxToken);
}
else if (SameSite == SameSiteMode.Strict)
{
AppendSegment(builder, SameSiteToken, SameSiteStrictToken);
}
if (HttpOnly)
@ -302,7 +337,7 @@ namespace Microsoft.Net.Http.Headers
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax|None}; httponly
private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue)
{
Contract.Requires(startIndex >= 0);
@ -437,25 +472,34 @@ namespace Microsoft.Net.Http.Headers
{
result.Secure = true;
}
// samesite-av = "SameSite" / "SameSite=" samesite-value
// samesite-value = "Strict" / "Lax"
// samesite-av = "SameSite=" samesite-value
// samesite-value = "Strict" / "Lax" / "None"
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
{
if (!ReadEqualsSign(input, ref offset))
{
result.SameSite = SameSiteMode.Strict;
result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.Unspecified;
}
else
{
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
if (StringSegment.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase))
{
result.SameSite = SameSiteMode.Strict;
}
else if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
{
result.SameSite = SameSiteMode.Lax;
}
else if (!SuppressSameSiteNone
&& StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase))
{
result.SameSite = SameSiteMode.None;
}
else
{
result.SameSite = SameSiteMode.Strict;
result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.Unspecified;
}
}
}

View File

@ -57,7 +57,7 @@ namespace Microsoft.Net.Http.Headers
{
SameSite = SameSiteMode.None,
};
dataset.Add(header7, "name7=value7");
dataset.Add(header7, "name7=value7; samesite=none");
return dataset;
@ -155,9 +155,20 @@ namespace Microsoft.Net.Http.Headers
{
SameSite = SameSiteMode.Strict
};
var string6a = "name6=value6; samesite";
var string6b = "name6=value6; samesite=Strict";
var string6c = "name6=value6; samesite=invalid";
var string6 = "name6=value6; samesite=Strict";
var header7 = new SetCookieHeaderValue("name7", "value7")
{
SameSite = SameSiteMode.None
};
var string7 = "name7=value7; samesite=None";
var header8 = new SetCookieHeaderValue("name8", "value8")
{
SameSite = SameSiteMode.Unspecified
};
var string8a = "name8=value8; samesite";
var string8b = "name8=value8; samesite=invalid";
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
@ -170,9 +181,10 @@ namespace Microsoft.Net.Http.Headers
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
dataset.Add(new[] { header5 }.ToList(), new[] { string5a });
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
dataset.Add(new[] { header6 }.ToList(), new[] { string6a });
dataset.Add(new[] { header6 }.ToList(), new[] { string6b });
dataset.Add(new[] { header6 }.ToList(), new[] { string6c });
dataset.Add(new[] { header6 }.ToList(), new[] { string6 });
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
return dataset;
}
@ -301,6 +313,28 @@ namespace Microsoft.Net.Http.Headers
Assert.Equal(expectedValue, input.ToString());
}
[Fact]
public void SetCookieHeaderValue_ToString_SameSiteNoneCompat()
{
SetCookieHeaderValue.SuppressSameSiteNone = true;
var input = new SetCookieHeaderValue("name", "value")
{
SameSite = SameSiteMode.None,
};
Assert.Equal("name=value", input.ToString());
SetCookieHeaderValue.SuppressSameSiteNone = false;
var input2 = new SetCookieHeaderValue("name", "value")
{
SameSite = SameSiteMode.None,
};
Assert.Equal("name=value; samesite=none", input2.ToString());
}
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
@ -312,6 +346,32 @@ namespace Microsoft.Net.Http.Headers
Assert.Equal(expectedValue, builder.ToString());
}
[Fact]
public void SetCookieHeaderValue_AppendToStringBuilder_SameSiteNoneCompat()
{
SetCookieHeaderValue.SuppressSameSiteNone = true;
var builder = new StringBuilder();
var input = new SetCookieHeaderValue("name", "value")
{
SameSite = SameSiteMode.None,
};
input.AppendToStringBuilder(builder);
Assert.Equal("name=value", builder.ToString());
SetCookieHeaderValue.SuppressSameSiteNone = false;
var builder2 = new StringBuilder();
var input2 = new SetCookieHeaderValue("name", "value")
{
SameSite = SameSiteMode.None,
};
input2.AppendToStringBuilder(builder2);
Assert.Equal("name=value; samesite=none", builder2.ToString());
}
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
@ -322,6 +382,31 @@ namespace Microsoft.Net.Http.Headers
Assert.Equal(expectedValue, header.ToString());
}
[Fact]
public void SetCookieHeaderValue_Parse_AcceptsValidValues_SameSiteNoneCompat()
{
SetCookieHeaderValue.SuppressSameSiteNone = true;
var header = SetCookieHeaderValue.Parse("name=value; samesite=none");
var cookie = new SetCookieHeaderValue("name", "value")
{
SameSite = SameSiteMode.Strict,
};
Assert.Equal(cookie, header);
Assert.Equal("name=value; samesite=strict", header.ToString());
SetCookieHeaderValue.SuppressSameSiteNone = false;
var header2 = SetCookieHeaderValue.Parse("name=value; samesite=none");
var cookie2 = new SetCookieHeaderValue("name", "value")
{
SameSite = SameSiteMode.None,
};
Assert.Equal(cookie2, header2);
Assert.Equal("name=value; samesite=none", header2.ToString());
}
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
@ -332,6 +417,31 @@ namespace Microsoft.Net.Http.Headers
Assert.Equal(expectedValue, header.ToString());
}
[Fact]
public void SetCookieHeaderValue_TryParse_AcceptsValidValues_SameSiteNoneCompat()
{
SetCookieHeaderValue.SuppressSameSiteNone = true;
Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header));
var cookie = new SetCookieHeaderValue("name", "value")
{
SameSite = SameSiteMode.Strict,
};
Assert.Equal(cookie, header);
Assert.Equal("name=value; samesite=strict", header.ToString());
SetCookieHeaderValue.SuppressSameSiteNone = false;
Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header2));
var cookie2 = new SetCookieHeaderValue("name", "value")
{
SameSite = SameSiteMode.None,
};
Assert.Equal(cookie2, header2);
Assert.Equal("name=value; samesite=none", header2.ToString());
}
[Theory]
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)

View File

@ -11,8 +11,20 @@ namespace Microsoft.AspNetCore.Http
/// </summary>
public class CookieBuilder
{
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
internal static bool SuppressSameSiteNone;
private string _name;
static CookieBuilder()
{
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
{
SuppressSameSiteNone = enabled;
}
}
/// <summary>
/// The name of the cookie.
/// </summary>
@ -49,12 +61,12 @@ namespace Microsoft.AspNetCore.Http
public virtual bool HttpOnly { get; set; }
/// <summary>
/// The SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.None"/>
/// The SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Unspecified"/>
/// </summary>
/// <remarks>
/// Determines the value that will set on <seealso cref="CookieOptions.SameSite"/>.
/// </remarks>
public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.None;
public virtual SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
/// <summary>
/// The policy that will be used to determine <seealso cref="CookieOptions.Secure"/>.

View File

@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Http
}
public enum SameSiteMode
{
Unspecified = -1,
None = 0,
Lax = 1,
Strict = 2,

View File

@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Http
}
public enum SameSiteMode
{
Unspecified = -1,
None = 0,
Lax = 1,
Strict = 2,

View File

@ -10,6 +10,18 @@ namespace Microsoft.AspNetCore.Http
/// </summary>
public class CookieOptions
{
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
internal static bool SuppressSameSiteNone;
static CookieOptions()
{
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
{
SuppressSameSiteNone = enabled;
}
}
/// <summary>
/// Creates a default cookie with a path of '/'.
/// </summary>
@ -43,10 +55,10 @@ namespace Microsoft.AspNetCore.Http
public bool Secure { get; set; }
/// <summary>
/// Gets or sets the value for the SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.None"/>
/// Gets or sets the value for the SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Unspecified"/>
/// </summary>
/// <returns>The <see cref="SameSiteMode"/> representing the enforcement mode of the cookie.</returns>
public SameSiteMode SameSite { get; set; } = SameSiteMode.None;
public SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
/// <summary>
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script.

View File

@ -5,12 +5,14 @@ namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// Used to set the SameSite field on response cookies to indicate if those cookies should be included by the client on future "same-site" or "cross-site" requests.
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
/// </summary>
// This mirrors Microsoft.Net.Http.Headers.SameSiteMode
public enum SameSiteMode
{
/// <summary>No SameSite field will be set, the client should follow its default cookie policy.</summary>
Unspecified = -1,
/// <summary>Indicates the client should disable same-site restrictions.</summary>
None = 0,
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
Lax,

View File

@ -113,6 +113,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebUtilities.Performance", "WebUtilities\perf\Microsoft.AspNetCore.WebUtilities.Performance\Microsoft.AspNetCore.WebUtilities.Performance.csproj", "{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization.Policy", "..\Security\Authorization\Policy\src\Microsoft.AspNetCore.Authorization.Policy.csproj", "{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cors", "..\Middleware\CORS\src\Microsoft.AspNetCore.Cors.csproj", "{09168958-FD5B-4D25-8FBF-75E2C80D903B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -603,6 +607,30 @@ Global
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x64.Build.0 = Release|Any CPU
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.ActiveCfg = Release|Any CPU
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.Build.0 = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x64.ActiveCfg = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x64.Build.0 = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x86.ActiveCfg = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x86.Build.0 = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|Any CPU.Build.0 = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x64.ActiveCfg = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x64.Build.0 = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x86.ActiveCfg = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x86.Build.0 = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x64.ActiveCfg = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x64.Build.0 = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x86.ActiveCfg = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x86.Build.0 = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|Any CPU.Build.0 = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x64.ActiveCfg = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x64.Build.0 = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x86.ActiveCfg = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -651,6 +679,8 @@ Global
{611794D2-EF3A-422A-A077-23E61C7ADE49} = {793FFE24-138A-4C3D-81AB-18D625E36230}
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0} = {793FFE24-138A-4C3D-81AB-18D625E36230}
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA}
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B} = {793FFE24-138A-4C3D-81AB-18D625E36230}
{09168958-FD5B-4D25-8FBF-75E2C80D903B} = {793FFE24-138A-4C3D-81AB-18D625E36230}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {85B5E151-2E9D-419C-83DD-0DDCF446C83A}

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -14,8 +13,8 @@ namespace Microsoft.AspNetCore.Routing
{
internal sealed class EndpointMiddleware
{
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked";
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareWithEndpointInvoked";
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
private readonly ILogger _logger;
private readonly RequestDelegate _next;
@ -91,7 +90,7 @@ namespace Microsoft.AspNetCore.Routing
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " +
"but a middleware was not found that supports authorization." +
Environment.NewLine +
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.");
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).");
}
private static void ThrowMissingCorsMiddlewareException(Endpoint endpoint)
@ -99,7 +98,7 @@ namespace Microsoft.AspNetCore.Routing
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " +
"but a middleware was not found that supports CORS." +
Environment.NewLine +
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code.");
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).");
}
private static class Log

View File

@ -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);
}
}
}

View File

@ -10,6 +10,8 @@
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
<Reference Include="Microsoft.AspNetCore.Cors" />
<Reference Include="Microsoft.AspNetCore.Routing" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>

View File

@ -101,7 +101,8 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var expected = "Endpoint Test contains authorization metadata, but a middleware was not found that supports authorization." +
Environment.NewLine +
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.";
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. " +
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()
@ -197,7 +198,8 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var expected = "Endpoint Test contains CORS metadata, but a middleware was not found that supports CORS." +
Environment.NewLine +
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code.";
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code. " +
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()

View File

@ -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"
]
}
}

View File

@ -14,8 +14,8 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
public class CorsMiddleware
{
// Property key is used by other systems, e.g. MVC, to check if CORS middleware has run
private const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";
private static readonly object CorsMiddlewareInvokedValue = new object();
private const string CorsMiddlewareWithEndpointInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
private static readonly object CorsMiddlewareWithEndpointInvokedValue = new object();
private readonly Func<object, Task> OnResponseStartingDelegate = OnResponseStarting;
private readonly RequestDelegate _next;
@ -116,14 +116,6 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
/// <inheritdoc />
public Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider)
{
// Flag to indicate to other systems, that CORS middleware was run for this request
context.Items[CorsMiddlewareInvokedKey] = CorsMiddlewareInvokedValue;
if (!context.Request.Headers.ContainsKey(CorsConstants.Origin))
{
return _next(context);
}
// CORS policy resolution rules:
//
// 1. If there is an endpoint with IDisableCorsAttribute then CORS is not run
@ -131,9 +123,20 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
// there is an endpoint with IEnableCorsAttribute that has a policy name then
// fetch policy by name, prioritizing it above policy on middleware
// 3. If there is no policy on middleware then use name on middleware
var endpoint = context.GetEndpoint();
if (endpoint != null)
{
// EndpointRoutingMiddleware uses this flag to check if the CORS middleware processed CORS metadata on the endpoint.
// The CORS middleware can only make this claim if it observes an actual endpoint.
context.Items[CorsMiddlewareWithEndpointInvokedKey] = CorsMiddlewareWithEndpointInvokedValue;
}
if (!context.Request.Headers.ContainsKey(CorsConstants.Origin))
{
return _next(context);
}
// Get the most significant CORS metadata for the endpoint
// For backwards compatibility reasons this is then downcast to Enable/Disable metadata
var corsMetadata = endpoint?.Metadata.GetMetadata<ICorsMetadata>();

View File

@ -918,7 +918,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
await middleware.Invoke(httpContext, mockProvider);
// Assert
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareInvoked"));
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
}
[Fact]
@ -936,12 +936,37 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
"DefaultPolicyName");
var httpContext = new DefaultHttpContext();
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint"));
// Act
await middleware.Invoke(httpContext, mockProvider);
// Assert
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareInvoked"));
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
}
[Fact]
public async Task Invoke_WithoutEndpoint_InvokeFlagSet()
{
// Arrange
var corsService = Mock.Of<ICorsService>();
var mockProvider = Mock.Of<ICorsPolicyProvider>();
var loggerFactory = NullLoggerFactory.Instance;
var middleware = new CorsMiddleware(
Mock.Of<RequestDelegate>(),
corsService,
loggerFactory,
"DefaultPolicyName");
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
// Act
await middleware.Invoke(httpContext, mockProvider);
// Assert
Assert.DoesNotContain(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
}
}
}

View File

@ -6,6 +6,7 @@ using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
@ -188,6 +189,12 @@ namespace Microsoft.AspNetCore.Builder
OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
});
// An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
// the endpoint and route values to ensure things are re-calculated.
context.HttpContext.SetEndpoint(endpoint: null);
var routeValuesFeature = context.HttpContext.Features.Get<IRouteValuesFeature>();
routeValuesFeature?.RouteValues?.Clear();
context.HttpContext.Request.Path = newPath;
context.HttpContext.Request.QueryString = newQueryString;
try

View File

@ -177,112 +177,6 @@ namespace Microsoft.AspNetCore.Diagnostics
}
}
[Fact]
public async Task Redirect_StatusPage()
{
var expectedStatusCode = 432;
var destination = "/location";
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseStatusCodePagesWithRedirects("/errorPage?id={0}");
app.Map(destination, (innerAppBuilder) =>
{
innerAppBuilder.Run((httpContext) =>
{
httpContext.Response.StatusCode = expectedStatusCode;
return Task.FromResult(1);
});
});
app.Map("/errorPage", (innerAppBuilder) =>
{
innerAppBuilder.Run(async (httpContext) =>
{
await httpContext.Response.WriteAsync(httpContext.Request.QueryString.Value);
});
});
app.Run((context) =>
{
throw new InvalidOperationException($"Invalid input provided. {context.Request.Path}");
});
});
var expectedQueryString = $"?id={expectedStatusCode}";
var expectedUri = $"/errorPage{expectedQueryString}";
using (var server = new TestServer(builder))
{
var client = server.CreateClient();
var response = await client.GetAsync(destination);
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
Assert.Equal(expectedUri, response.Headers.First(s => s.Key == "Location").Value.First());
response = await client.GetAsync(expectedUri);
var content = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedQueryString, content);
Assert.Equal(expectedQueryString, response.RequestMessage.RequestUri.Query);
}
}
[Fact]
public async Task Reexecute_CanRetrieveInformationAboutOriginalRequest()
{
var expectedStatusCode = 432;
var destination = "/location";
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Use(async (context, next) =>
{
var beforeNext = context.Request.QueryString;
await next();
var afterNext = context.Request.QueryString;
Assert.Equal(beforeNext, afterNext);
});
app.UseStatusCodePagesWithReExecute(pathFormat: "/errorPage", queryFormat: "?id={0}");
app.Map(destination, (innerAppBuilder) =>
{
innerAppBuilder.Run((httpContext) =>
{
httpContext.Response.StatusCode = expectedStatusCode;
return Task.FromResult(1);
});
});
app.Map("/errorPage", (innerAppBuilder) =>
{
innerAppBuilder.Run(async (httpContext) =>
{
var statusCodeReExecuteFeature = httpContext.Features.Get<IStatusCodeReExecuteFeature>();
await httpContext.Response.WriteAsync(
httpContext.Request.QueryString.Value
+ ", "
+ statusCodeReExecuteFeature.OriginalPath
+ ", "
+ statusCodeReExecuteFeature.OriginalQueryString);
});
});
app.Run((context) =>
{
throw new InvalidOperationException("Invalid input provided.");
});
});
using (var server = new TestServer(builder))
{
var client = server.CreateClient();
var response = await client.GetAsync(destination + "?name=James");
var content = await response.Content.ReadAsStringAsync();
Assert.Equal($"?id={expectedStatusCode}, /location, ?name=James", content);
}
}
[Fact]
public async Task ClearsCacheHeaders_SetByReexecutionPathHandlers()
{

View File

@ -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);
}
}
}

View File

@ -94,11 +94,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
private static async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
if (parametersCollection.GetEnumerator().MoveNext())
{
throw new InvalidOperationException("Prerendering server components with parameters is not supported.");
}
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
@ -106,6 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
var currentInvocation = invocationSerializer.SerializeInvocation(
invocationId,
type,
parametersCollection,
prerendered: true);
var result = await prerenderer.PrerenderComponentAsync(
@ -121,14 +117,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
if (parametersCollection.GetEnumerator().MoveNext())
{
throw new InvalidOperationException("Server components with parameters are not supported.");
}
var serviceProvider = context.RequestServices;
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, prerendered: false);
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
}

View File

@ -34,6 +34,7 @@
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
</ItemGroup>
</Project>

View File

@ -19,22 +19,27 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, bool prerendered)
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, bool prerendered)
{
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type);
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters);
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
}
private (int sequence, string payload) CreateSerializedServerComponent(
ServerComponentInvocationSequence invocationId,
Type rootComponent)
Type rootComponent,
ParameterView parameters)
{
var sequence = invocationId.Next();
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
var serverComponent = new ServerComponent(
sequence,
rootComponent.Assembly.GetName().Name,
rootComponent.FullName,
definitions,
values,
invocationId.Value);
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);

View File

@ -188,25 +188,208 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
Assert.Equal("<p>Hello Steve!</p>", content);
}
[Theory]
[InlineData(RenderMode.Server, "Server components with parameters are not supported.")]
[InlineData(RenderMode.ServerPrerendered, "Prerendering server components with parameters is not supported.")]
public async Task ComponentWithParametersObject_ThrowsInvalidOperationExceptionForServerRenderModes(
RenderMode renderMode,
string expectedMessage)
[Fact]
public async Task CanRender_ComponentWithParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act & Assert
var result = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<GreetingComponent>(
renderMode,
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Server,
new
{
Name = "Steve"
}));
Assert.Equal(expectedMessage, result.Message);
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern);
// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<ServerComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, marker.Sequence);
Assert.Null(marker.PrerenderId);
Assert.NotNull(marker.Descriptor);
Assert.Equal("server", marker.Type);
var unprotectedServerComponent = protector.Unprotect(marker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Name", parameterDefinition.Name);
Assert.Equal("System.String", parameterDefinition.TypeName);
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);
var rawValue = Assert.IsType<JsonElement>(value);
Assert.Equal("Daniel", rawValue.GetString());
}
[Fact]
public async Task CanRender_ComponentWithNullParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Server,
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern);
// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<ServerComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, marker.Sequence);
Assert.Null(marker.PrerenderId);
Assert.NotNull(marker.Descriptor);
Assert.Equal("server", marker.Type);
var unprotectedServerComponent = protector.Unprotect(marker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
Assert.NotNull(serverComponent.ParameterDefinitions);
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Name", parameterDefinition.Name);
Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);;
Assert.Null(value);
}
[Fact]
public async Task CanPrerender_ComponentWithParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.ServerPrerendered,
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<ServerComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, preambleMarker.Sequence);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.NotNull(preambleMarker.Descriptor);
Assert.Equal("server", preambleMarker.Type);
var unprotectedServerComponent = protector.Unprotect(preambleMarker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotEqual(default, serverComponent);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Name", parameterDefinition.Name);
Assert.Equal("System.String", parameterDefinition.TypeName);
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);
var rawValue = Assert.IsType<JsonElement>(value);
Assert.Equal("Daniel", rawValue.GetString());
var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<p>Hello Daniel!</p>", prerenderedContent);
var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<ServerComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
Assert.Null(epilogueMarker.Sequence);
Assert.Null(epilogueMarker.Descriptor);
Assert.Null(epilogueMarker.Type);
}
[Fact]
public async Task CanPrerender_ComponentWithNullParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.ServerPrerendered,
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<ServerComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, preambleMarker.Sequence);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.NotNull(preambleMarker.Descriptor);
Assert.Equal("server", preambleMarker.Type);
var unprotectedServerComponent = protector.Unprotect(preambleMarker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotEqual(default, serverComponent);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
Assert.NotNull(serverComponent.ParameterDefinitions);
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Name", parameterDefinition.Name);
Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);
Assert.Null(value);
var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<p>Hello (null)!</p>", prerenderedContent);
var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<ServerComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
Assert.Null(epilogueMarker.Sequence);
Assert.Null(epilogueMarker.Descriptor);
Assert.Null(epilogueMarker.Type);
}
[Fact]
@ -547,7 +730,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var s = 0;
base.BuildRenderTree(builder);
builder.OpenElement(s++, "p");
builder.AddContent(s++, $"Hello {Name}!");
builder.AddContent(s++, $"Hello {Name ?? ("(null)")}!");
builder.CloseElement();
}
}

View File

@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorComponents
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponentAsync_MarksSelectedOptionsAsSelected()
{
@ -668,18 +668,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorComponents
public Task SetParametersAsync(ParameterView parameters)
{
_renderHandle.Render(CreateRenderFragment(parameters));
var content = parameters.GetValueOrDefault<string>("Value");
_renderHandle.Render(CreateRenderFragment(content));
return Task.CompletedTask;
}
private RenderFragment CreateRenderFragment(ParameterView parameters)
private RenderFragment CreateRenderFragment(string content)
{
return RenderFragment;
void RenderFragment(RenderTreeBuilder rtb)
{
rtb.OpenElement(1, "span");
rtb.AddContent(2, parameters.GetValueOrDefault<string>("Value"));
rtb.AddContent(2, content);
rtb.CloseElement();
}
}

View File

@ -9,10 +9,10 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "A project template for creating a Blazor server app that runs server-side inside an ASP.NET Core app and handles user interactions over a SignalR connection. This template can be used for web apps with rich dynamic user interfaces (UIs).",
"groupIdentity": "Microsoft.Web.Blazor.Server",
"precedence": "5000",
"identity": "Microsoft.Web.Blazor.Server.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.Blazor.Server.CSharp.3.1",
"shortName": "blazorserver",
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
"tags": {
"language": "C#",
"type": "project"

View File

@ -1,22 +1,25 @@
<Router AppAssembly="@typeof(Program).Assembly">
@*#if (NoAuth)
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
@*#if (!NoAuth)
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
#else
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
#endif*@
</Found>
<NotFound>
@*#if (!NoAuth)
<CascadingAuthenticationState>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</CascadingAuthenticationState>
#else
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
#endif*@
</NotFound>
</Router>
#else
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
#endif*@

View File

@ -9,8 +9,8 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "An empty project template for creating an ASP.NET Core application. This template does not have any content in it.",
"groupIdentity": "Microsoft.Web.Empty",
"precedence": "5000",
"identity": "Microsoft.Web.Empty.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.Empty.CSharp.3.1",
"shortName": "web",
"tags": {
"language": "C#",

View File

@ -8,8 +8,8 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "An empty project template for creating an ASP.NET Core application. This template does not have any content in it.",
"groupIdentity": "Microsoft.Web.Empty",
"precedence": "5000",
"identity": "Microsoft.Web.Empty.FSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.Empty.FSharp.3.1",
"shortName": "web",
"tags": {
"language": "F#",

View File

@ -9,8 +9,8 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "A project template for creating a gRPC ASP.NET Core service.",
"groupIdentity": "Microsoft.Web.Grpc",
"precedence": "5000",
"identity": "Microsoft.Grpc.Service.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Grpc.Service.CSharp.3.1",
"shortName": "grpc",
"tags": {
"language": "C#",

View File

@ -11,8 +11,8 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "A project for creating a Razor class library that targets .NET Standard",
"groupIdentity": "Microsoft.Web.Razor",
"precedence": "5000",
"identity": "Microsoft.Web.Razor.Library.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.Razor.Library.CSharp.3.1",
"shortName": "razorclasslib",
"tags": {
"language": "C#",

View File

@ -10,13 +10,13 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "A project template for creating an ASP.NET Core application with example ASP.NET Core Razor Pages content",
"groupIdentity": "Microsoft.Web.RazorPages",
"precedence": "5000",
"identity": "Microsoft.Web.RazorPages.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.RazorPages.CSharp.3.1",
"shortName": [
"webapp",
"razor"
],
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
"tags": {
"language": "C#",
"type": "project"

View File

@ -9,10 +9,10 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "A project template for creating an ASP.NET Core application with example ASP.NET Core MVC Views and Controllers. This template can also be used for RESTful HTTP services.",
"groupIdentity": "Microsoft.Web.Mvc",
"precedence": "5000",
"identity": "Microsoft.Web.Mvc.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.Mvc.CSharp.3.1",
"shortName": "mvc",
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
"tags": {
"language": "C#",
"type": "project"

View File

@ -9,10 +9,10 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "A project template for creating an ASP.NET Core application with example ASP.NET Core MVC Views and Controllers. This template can also be used for RESTful HTTP services.",
"groupIdentity": "Microsoft.Web.Mvc",
"precedence": "5000",
"identity": "Microsoft.Web.Mvc.FSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.Mvc.FSharp.3.1",
"shortName": "mvc",
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.0-third-party-notices",
"thirdPartyNotices": "https://aka.ms/aspnetcore/3.1-third-party-notices",
"tags": {
"language": "F#",
"type": "project"

View File

@ -9,8 +9,8 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "A project template for creating an ASP.NET Core application with an example Controller for a RESTful HTTP service. This template can also be used for ASP.NET Core MVC Views and Controllers.",
"groupIdentity": "Microsoft.Web.WebApi",
"precedence": "5000",
"identity": "Microsoft.Web.WebApi.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.WebApi.CSharp.3.1",
"shortName": "webapi",
"tags": {
"language": "C#",

View File

@ -8,8 +8,8 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "A project template for creating an ASP.NET Core application with an example Controller for a RESTful HTTP service. This template can also be used for ASP.NET Core MVC Views and Controllers.",
"groupIdentity": "Microsoft.Web.WebApi",
"precedence": "5000",
"identity": "Microsoft.Web.WebApi.FSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Web.WebApi.FSharp.3.1",
"shortName": "webapi",
"tags": {
"language": "F#",

View File

@ -10,8 +10,8 @@
"generatorVersions": "[1.0.0.0-*)",
"description": "An empty project template for creating a worker service.",
"groupIdentity": "Microsoft.Worker.Empty",
"precedence": "5000",
"identity": "Microsoft.Worker.Empty.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.Worker.Empty.CSharp.3.1",
"shortName": "worker",
"tags": {
"language": "C#",

View File

@ -6,8 +6,8 @@
"SPA"
],
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular",
"precedence": "5000",
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.Angular.CSharp.3.1",
"name": "ASP.NET Core with Angular",
"preferNameDirectory": true,
"primaryOutputs": [

View File

@ -71,7 +71,7 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": ["styles.css"],
"styles": ["src/styles.css"],
"scripts": [],
"assets": ["src/assets"]
}

View File

@ -4750,9 +4750,9 @@
"dev": true
},
"handlebars": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",

View File

@ -6,8 +6,8 @@
"SPA"
],
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React",
"precedence": "5000",
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.React.CSharp.3.1",
"name": "ASP.NET Core with React.js",
"preferNameDirectory": true,
"primaryOutputs": [

View File

@ -4868,9 +4868,12 @@
}
},
"eslint-utils": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
"integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q=="
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
"requires": {
"eslint-visitor-keys": "^1.0.0"
}
},
"eslint-visitor-keys": {
"version": "1.0.0",
@ -5664,9 +5667,9 @@
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
},
"handlebars": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
"requires": {
"neo-async": "^2.6.0",
"optimist": "^0.6.1",

View File

@ -6,8 +6,8 @@
"SPA"
],
"groupIdentity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux",
"precedence": "5000",
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux.CSharp.3.0",
"precedence": "6000",
"identity": "Microsoft.DotNet.Web.Spa.ProjectTemplates.ReactRedux.CSharp.3.1",
"name": "ASP.NET Core with React.js and Redux",
"preferNameDirectory": true,
"primaryOutputs": [

View File

@ -5044,9 +5044,12 @@
}
},
"eslint-utils": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
"integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q=="
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
"requires": {
"eslint-visitor-keys": "^1.0.0"
}
},
"eslint-visitor-keys": {
"version": "1.0.0",
@ -5862,9 +5865,9 @@
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
},
"handlebars": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
"requires": {
"neo-async": "^2.6.0",
"optimist": "^0.6.1",

View File

@ -104,14 +104,14 @@ namespace Templates.Test.Helpers
return new ProcessEx(output, proc);
}
public static async Task<ProcessEx> RunViaShellAsync(ITestOutputHelper output, string workingDirectory, string commandAndArgs)
public static ProcessEx RunViaShell(ITestOutputHelper output, string workingDirectory, string commandAndArgs)
{
var (shellExe, argsPrefix) = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? ("cmd", "/c")
: ("bash", "-c");
var result = Run(output, workingDirectory, shellExe, $"{argsPrefix} \"{commandAndArgs}\"");
await result.Exited;
result.WaitForExit(assertSuccess: false);
return result;
}
@ -168,9 +168,14 @@ namespace Templates.Test.Helpers
return $"Process exited with code {_process.ExitCode}\nStdErr: {Error}\nStdOut: {Output}";
}
public void WaitForExit(bool assertSuccess)
public void WaitForExit(bool assertSuccess, TimeSpan? timeSpan = null)
{
Exited.Wait();
if(!timeSpan.HasValue)
{
timeSpan = TimeSpan.FromSeconds(480);
}
Exited.Wait(timeSpan.Value);
if (assertSuccess && _process.ExitCode != 0)
{

View File

@ -287,7 +287,7 @@ namespace Templates.Test.Helpers
try
{
output.WriteLine($"Restoring NPM packages in '{workingDirectory}' using npm...");
var result = await ProcessEx.RunViaShellAsync(output, workingDirectory, "npm install");
var result = ProcessEx.RunViaShell(output, workingDirectory, "npm install");
return result;
}
finally

Some files were not shown because too many files have changed in this diff Show More