Merge pull request #24217 from dotnet-maestro-bot/merge/release/5.0-preview8-to-master
[automated] Merge branch 'release/5.0-preview8' => 'master'
This commit is contained in:
commit
7a9707eb98
|
|
@ -1427,6 +1427,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions.Tests", "src\Components\Web.Extensions\test\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj", "{157605CB-5170-4C1A-980F-4BAE42DB60DE}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{2531F00A-54EB-4074-9C0B-9AF9FB3679DC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicTestApp", "src\Components\test\testassets\BasicTestApp\BasicTestApp.csproj", "{85D67E40-4B11-48ED-8C43-34590A1FB9ED}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LazyTestContentPackage", "src\Components\test\testassets\LazyTestContentPackage\LazyTestContentPackage.csproj", "{C0EF53A5-5A94-4849-86B0-2297EA08D649}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentsApp.App", "src\Components\test\testassets\ComponentsApp.App\ComponentsApp.App.csproj", "{4FDD820F-8397-41B7-956E-F257DD044BD8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentsApp.Server", "src\Components\test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{CA7C7A53-446F-453A-A57B-78BB1443B8A8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestContentPackage", "src\Components\test\testassets\TestContentPackage\TestContentPackage.csproj", "{B32C5882-2313-40D0-A003-2FF33724CFE6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.TestServer", "src\Components\test\testassets\TestServer\Components.TestServer.csproj", "{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk", "Sdk", "{FED4267E-E5E4-49C5-98DB-8B3F203596EE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly", "src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", "{6B2734BF-C61D-4889-ABBF-456A4075D59B}"
|
||||
|
|
@ -6763,6 +6777,78 @@ Global
|
|||
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Release|x64.Build.0 = Release|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Release|x64.Build.0 = Release|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
|
@ -7634,6 +7720,13 @@ Global
|
|||
{F71FE795-9923-461B-9809-BB1821A276D0} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
|
||||
{8294A74F-7DAA-4B69-BC56-7634D93C9693} = {F71FE795-9923-461B-9809-BB1821A276D0}
|
||||
{157605CB-5170-4C1A-980F-4BAE42DB60DE} = {F71FE795-9923-461B-9809-BB1821A276D0}
|
||||
{2531F00A-54EB-4074-9C0B-9AF9FB3679DC} = {0508E463-0269-40C9-B5C2-3B600FB2A28B}
|
||||
{85D67E40-4B11-48ED-8C43-34590A1FB9ED} = {2531F00A-54EB-4074-9C0B-9AF9FB3679DC}
|
||||
{C0EF53A5-5A94-4849-86B0-2297EA08D649} = {2531F00A-54EB-4074-9C0B-9AF9FB3679DC}
|
||||
{4FDD820F-8397-41B7-956E-F257DD044BD8} = {2531F00A-54EB-4074-9C0B-9AF9FB3679DC}
|
||||
{CA7C7A53-446F-453A-A57B-78BB1443B8A8} = {2531F00A-54EB-4074-9C0B-9AF9FB3679DC}
|
||||
{B32C5882-2313-40D0-A003-2FF33724CFE6} = {2531F00A-54EB-4074-9C0B-9AF9FB3679DC}
|
||||
{26F88A06-319C-43F3-9FD9-8BC2D29F8C00} = {2531F00A-54EB-4074-9C0B-9AF9FB3679DC}
|
||||
{FED4267E-E5E4-49C5-98DB-8B3F203596EE} = {562D5067-8CD8-4F19-BCBB-873204932C61}
|
||||
{6B2734BF-C61D-4889-ABBF-456A4075D59B} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
|
||||
{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
|
||||
|
|
|
|||
|
|
@ -68,12 +68,12 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
/// <summary>
|
||||
/// Get or sets the content to display when asynchronous navigation is in progress.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment Navigating { get; set; }
|
||||
[Parameter] public RenderFragment? Navigating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a handler that should be called before navigating to a new page.
|
||||
/// </summary>
|
||||
[Parameter] public Func<NavigationContext, Task> OnNavigateAsync { get; set; }
|
||||
[Parameter] public Func<NavigationContext, Task>? OnNavigateAsync { get; set; }
|
||||
|
||||
private RouteTable Routes { get; set; }
|
||||
|
||||
|
|
@ -195,10 +195,6 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
|
||||
private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNavigate)
|
||||
{
|
||||
if (OnNavigateAsync == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cancel the CTS instead of disposing it, since disposing does not
|
||||
// actually cancel and can cause unintended Object Disposed Exceptions.
|
||||
|
|
@ -210,6 +206,11 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
// invocation.
|
||||
await previousOnNavigate;
|
||||
|
||||
if (OnNavigateAsync == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_onNavigateCts = new CancellationTokenSource();
|
||||
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
|
||||
|
||||
|
|
@ -227,14 +228,12 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
if (e.CancellationToken != navigateContext.CancellationToken)
|
||||
{
|
||||
var rethrownException = new InvalidOperationException("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", e);
|
||||
_renderHandle.Render(builder => ExceptionDispatchInfo.Capture(rethrownException).Throw());
|
||||
return false;
|
||||
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(rethrownException));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_renderHandle.Render(builder => ExceptionDispatchInfo.Capture(e).Throw());
|
||||
return false;
|
||||
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(e));
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
{
|
||||
internal static class HeadManagementJSRuntimeExtensions
|
||||
{
|
||||
private const string JsFunctionsPrefix = "_blazorHeadManager";
|
||||
|
||||
public static ValueTask SetTitleAsync(this IJSRuntime jsRuntime, string title)
|
||||
{
|
||||
return jsRuntime.InvokeVoidAsync($"{JsFunctionsPrefix}.setTitle", title);
|
||||
}
|
||||
|
||||
public static ValueTask AddOrUpdateHeadTagAsync(this IJSRuntime jsRuntime, TagElement tag, string id)
|
||||
{
|
||||
return jsRuntime.InvokeVoidAsync($"{JsFunctionsPrefix}.addOrUpdateHeadTag", tag, id);
|
||||
}
|
||||
|
||||
public static ValueTask RemoveHeadTagAsync(this IJSRuntime jsRuntime, string id)
|
||||
{
|
||||
return jsRuntime.InvokeVoidAsync($"{JsFunctionsPrefix}.removeHeadTag", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
// 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.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
{
|
||||
/// <summary>
|
||||
/// Serves as a base for components that represent tags in the HTML head.
|
||||
/// </summary>
|
||||
public abstract class HeadTagBase : ComponentBase, IDisposable
|
||||
{
|
||||
private readonly string _id = Guid.NewGuid().ToString("N");
|
||||
|
||||
private TagElement _tagElement;
|
||||
|
||||
private bool _hasRendered;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JSRuntime { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of additional attributes that will be applied to the meta element.
|
||||
/// </summary>
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public IReadOnlyDictionary<string, object>? Attributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the tag being represented.
|
||||
/// </summary>
|
||||
protected abstract string TagName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
_tagElement = new TagElement(TagName, Attributes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
_hasRendered = true;
|
||||
|
||||
await JSRuntime.AddOrUpdateHeadTagAsync(_tagElement, _id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddMarkupContent(0, $"<!--Head:{JsonSerializer.Serialize(_tagElement, JsonSerializerOptionsProvider.Options)}-->");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_hasRendered)
|
||||
{
|
||||
_ = JSRuntime.RemoveHeadTagAsync(_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
{
|
||||
/// <summary>
|
||||
/// A component that adds a link tag to the HTML head.
|
||||
/// </summary>
|
||||
public sealed class Link : HeadTagBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string TagName => "link";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
{
|
||||
/// <summary>
|
||||
/// A component that adds a meta tag to the HTML head.
|
||||
/// </summary>
|
||||
public sealed class Meta : HeadTagBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string TagName => "meta";
|
||||
}
|
||||
}
|
||||
|
|
@ -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 System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
{
|
||||
internal readonly struct TagElement
|
||||
{
|
||||
public string Type => "tag";
|
||||
|
||||
public string TagName { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, object>? Attributes { get; }
|
||||
|
||||
public TagElement(string tagName, IReadOnlyDictionary<string, object>? attributes)
|
||||
{
|
||||
TagName = tagName;
|
||||
Attributes = attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
{
|
||||
/// <summary>
|
||||
/// A component that changes the title of the document.
|
||||
/// </summary>
|
||||
public sealed class Title : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private IJSRuntime JSRuntime { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value to use as the document's title.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await JSRuntime.SetTitleAsync(Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddMarkupContent(0, $"<!--Head:{JsonSerializer.Serialize(new TitleElement(Value), JsonSerializerOptionsProvider.Options)}-->");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
{
|
||||
internal readonly struct TitleElement
|
||||
{
|
||||
public string Type => "title";
|
||||
|
||||
public string Title { get; }
|
||||
|
||||
public TitleElement(string title)
|
||||
{
|
||||
Title = title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Components" />
|
||||
<Reference Include="Microsoft.AspNetCore.DataProtection" />
|
||||
<Reference Include="Microsoft.JSInterop" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
(function () {
|
||||
// Local helpers
|
||||
|
||||
const blazorIdAttributeName = '_blazor_id';
|
||||
const headCommentRegularExpression = /\W*Head:[^{]*(.*)$/;
|
||||
const prerenderedTags = [];
|
||||
|
||||
function createHeadTag({ tagName, attributes }, id) {
|
||||
const tagElement = document.createElement(tagName);
|
||||
|
||||
// The id is undefined during prerendering
|
||||
if (id) {
|
||||
tagElement.setAttribute(blazorIdAttributeName, id);
|
||||
}
|
||||
|
||||
if (attributes) {
|
||||
Object.keys(attributes).forEach(key => {
|
||||
tagElement.setAttribute(key, attributes[key]);
|
||||
});
|
||||
}
|
||||
|
||||
document.head.appendChild(tagElement);
|
||||
|
||||
return tagElement;
|
||||
}
|
||||
|
||||
function resolvePrerenderedHeadComponents(node) {
|
||||
node.childNodes.forEach((childNode) => {
|
||||
const headElement = parseHeadComment(childNode);
|
||||
|
||||
if (headElement) {
|
||||
applyPrerenderedHeadComponent(headElement);
|
||||
} else {
|
||||
resolvePrerenderedHeadComponents(childNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyPrerenderedHeadComponent(headElement) {
|
||||
switch (headElement.type) {
|
||||
case 'title':
|
||||
setTitle(headElement.title);
|
||||
break;
|
||||
case 'tag':
|
||||
const tag = createHeadTag(headElement);
|
||||
prerenderedTags.push(tag);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid head element type '${headElement.type}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseHeadComment(node) {
|
||||
if (!node || node.nodeType != Node.COMMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentText = node.textContent;
|
||||
|
||||
if (!commentText) {
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = headCommentRegularExpression.exec(commentText);
|
||||
const json = definition && definition[1];
|
||||
|
||||
return json && JSON.parse(json);
|
||||
}
|
||||
|
||||
function removePrerenderedHeadTags() {
|
||||
prerenderedTags.forEach((tag) => {
|
||||
tag.remove();
|
||||
});
|
||||
|
||||
prerenderedTags.length = 0;
|
||||
}
|
||||
|
||||
// Exported functions
|
||||
|
||||
function setTitle(title) {
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
function addOrUpdateHeadTag(tag, id) {
|
||||
removePrerenderedHeadTags();
|
||||
removeHeadTag(id);
|
||||
createHeadTag(tag, id);
|
||||
}
|
||||
|
||||
function removeHeadTag(id) {
|
||||
let tag = document.head.querySelector(`[${blazorIdAttributeName}='${id}']`);
|
||||
tag && tag.remove();
|
||||
}
|
||||
|
||||
window._blazorHeadManager = {
|
||||
setTitle,
|
||||
addOrUpdateHeadTag,
|
||||
removeHeadTag,
|
||||
};
|
||||
|
||||
resolvePrerenderedHeadComponents(document);
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -324,9 +324,18 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
|
|||
const assembliesToLoad = BINDING.mono_array_to_js_array<System_String, string>(assembliesToLoadDotNetArray);
|
||||
const lazyAssemblies = resourceLoader.bootConfig.resources.lazyAssembly;
|
||||
|
||||
if (lazyAssemblies) {
|
||||
const resourcePromises = Promise.all(assembliesToLoad
|
||||
.filter(assembly => lazyAssemblies.hasOwnProperty(assembly))
|
||||
if (!lazyAssemblies) {
|
||||
throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly.");
|
||||
}
|
||||
|
||||
var assembliesMarkedAsLazy = assembliesToLoad.filter(assembly => lazyAssemblies.hasOwnProperty(assembly));
|
||||
|
||||
if (assembliesMarkedAsLazy.length != assembliesToLoad.length) {
|
||||
var notMarked = assembliesToLoad.filter(assembly => !assembliesMarkedAsLazy.includes(assembly));
|
||||
throw new Error(`${notMarked.join()} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
|
||||
}
|
||||
|
||||
const resourcePromises = Promise.all(assembliesMarkedAsLazy
|
||||
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
|
||||
.map(async resource => (await resource.response).arrayBuffer()));
|
||||
|
||||
|
|
@ -345,8 +354,6 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
|
|||
return resourcesToLoad.length;
|
||||
}));
|
||||
}
|
||||
return BINDING.js_to_mono_obj(Promise.resolve(0));
|
||||
}
|
||||
});
|
||||
|
||||
module.postRun.push(() => {
|
||||
|
|
|
|||
|
|
@ -132,8 +132,10 @@ export function removeLogicalChild(parent: LogicalElement, childIndex: number) {
|
|||
// If it's a logical container, also remove its descendants
|
||||
if (childToRemove instanceof Comment) {
|
||||
const grandchildrenArray = getLogicalChildrenArray(childToRemove);
|
||||
while (grandchildrenArray.length > 0) {
|
||||
removeLogicalChild(childToRemove, 0);
|
||||
if (grandchildrenArray) {
|
||||
while (grandchildrenArray.length > 0) {
|
||||
removeLogicalChild(childToRemove, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
/// <summary>
|
||||
/// Gets the logging builder for configuring logging services.
|
||||
/// </summary>
|
||||
public ILoggingBuilder Logging { get; }
|
||||
public ILoggingBuilder Logging { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="IServiceProviderFactory{TBuilder}" /> instance to be used to create the <see cref="IServiceProvider" />.
|
||||
|
|
@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance);
|
||||
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
|
||||
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
|
||||
Services.AddSingleton(provider => new LazyAssemblyLoader(provider));
|
||||
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
|
||||
Services.AddLogging(builder => {
|
||||
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using System.Reflection;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.JSInterop;
|
||||
using Microsoft.JSInterop.WebAssembly;
|
||||
|
||||
|
|
@ -20,19 +19,18 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
|||
///
|
||||
/// Supports finding pre-loaded assemblies in a server or pre-rendering context.
|
||||
/// </summary>
|
||||
public class LazyAssemblyLoader
|
||||
public sealed class LazyAssemblyLoader
|
||||
{
|
||||
internal const string GetDynamicAssemblies = "window.Blazor._internal.getLazyAssemblies";
|
||||
internal const string ReadDynamicAssemblies = "window.Blazor._internal.readLazyAssemblies";
|
||||
|
||||
private List<Assembly> _loadedAssemblyCache = new List<Assembly>();
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly HashSet<string> _loadedAssemblyCache;
|
||||
|
||||
private readonly IServiceProvider _provider;
|
||||
|
||||
public LazyAssemblyLoader(IServiceProvider provider)
|
||||
public LazyAssemblyLoader(IJSRuntime jsRuntime)
|
||||
{
|
||||
_provider = provider;
|
||||
_loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().ToList();
|
||||
_jsRuntime = jsRuntime;
|
||||
_loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name + ".dll").ToHashSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -55,37 +53,45 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
|||
|
||||
private Task<IEnumerable<Assembly>> LoadAssembliesInServerAsync(IEnumerable<string> assembliesToLoad)
|
||||
{
|
||||
var loadedAssemblies = _loadedAssemblyCache.Where(assembly =>
|
||||
assembliesToLoad.Contains(assembly.GetName().Name + ".dll"));
|
||||
var loadedAssemblies = new List<Assembly>();
|
||||
|
||||
if (loadedAssemblies.Count() != assembliesToLoad.Count())
|
||||
try
|
||||
{
|
||||
var unloadedAssemblies = assembliesToLoad.Except(loadedAssemblies.Select(a => a.GetName().Name + ".dll"));
|
||||
throw new InvalidOperationException($"Unable to find the following assemblies: {string.Join(",", unloadedAssemblies)}. Make sure that the appplication is referencing the assemblies and that they are present in the output folder.");
|
||||
foreach (var assemblyName in assembliesToLoad)
|
||||
{
|
||||
loadedAssemblies.Add(Assembly.Load(Path.GetFileNameWithoutExtension(assemblyName)));
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to find the following assembly: {ex.FileName}. Make sure that the appplication is referencing the assemblies and that they are present in the output folder.");
|
||||
}
|
||||
|
||||
return Task.FromResult(loadedAssemblies);
|
||||
return Task.FromResult<IEnumerable<Assembly>>(loadedAssemblies);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerable<string> assembliesToLoad)
|
||||
{
|
||||
var jsRuntime = _provider.GetRequiredService<IJSRuntime>();
|
||||
// Only load assemblies that haven't already been lazily-loaded
|
||||
var newAssembliesToLoad = assembliesToLoad.Except(_loadedAssemblyCache.Select(a => a.GetName().Name + ".dll"));
|
||||
// Check to see if the assembly has already been loaded and avoids reloading it if so.
|
||||
// Note: in the future, as an extra precuation, we can call `Assembly.Load` and check
|
||||
// to see if it throws FileNotFound to ensure that an assembly hasn't been loaded
|
||||
// between when the cache of loaded assemblies was instantiated in the constructor
|
||||
// and the invocation of this method.
|
||||
var newAssembliesToLoad = assembliesToLoad.Where(assembly => !_loadedAssemblyCache.Contains(assembly));
|
||||
var loadedAssemblies = new List<Assembly>();
|
||||
|
||||
var count = (int)await ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
|
||||
GetDynamicAssemblies,
|
||||
assembliesToLoad.ToArray(),
|
||||
null,
|
||||
null);
|
||||
var count = (int)await ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
|
||||
GetDynamicAssemblies,
|
||||
newAssembliesToLoad.ToArray(),
|
||||
null,
|
||||
null);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return loadedAssemblies;
|
||||
}
|
||||
|
||||
var assemblies = ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
|
||||
var assemblies = ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
|
||||
ReadDynamicAssemblies,
|
||||
null,
|
||||
null,
|
||||
|
|
@ -99,7 +105,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
|||
// into the default app context.
|
||||
var loadedAssembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly));
|
||||
loadedAssemblies.Add(loadedAssembly);
|
||||
_loadedAssemblyCache.Add(loadedAssembly);
|
||||
_loadedAssemblyCache.Add(loadedAssembly.GetName().Name + ".dll");
|
||||
}
|
||||
|
||||
return loadedAssemblies;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,51 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
AssertLogDoesNotContainCriticalMessages("Could not load file or assembly 'Newtonsoft.Json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanInfluenceHeadDuringPrerender()
|
||||
{
|
||||
Navigate("/prerendered/prerendered-head");
|
||||
|
||||
var metaWithBindings = Browser.FindElement(By.Id("meta-with-bindings"));
|
||||
var metaNoBindings = Browser.FindElement(By.Id("meta-no-bindings"));
|
||||
|
||||
// Validate updated head during prerender
|
||||
Browser.Equal("Initial title", () => Browser.Title);
|
||||
Browser.Equal("Initial meta content", () => metaWithBindings.GetAttribute("content"));
|
||||
Browser.Equal("Immutable meta content", () => metaNoBindings.GetAttribute("content"));
|
||||
|
||||
BeginInteractivity();
|
||||
|
||||
// Wait for elements to be recreated with internal ids to permit mutation
|
||||
metaWithBindings = WaitForNewElement(metaWithBindings, "meta-with-bindings");
|
||||
metaNoBindings = WaitForNewElement(metaNoBindings, "meta-no-bindings");
|
||||
|
||||
// Validate updated head after prerender
|
||||
Browser.Equal("Initial title", () => Browser.Title);
|
||||
Browser.Equal("Initial meta content", () => metaWithBindings.GetAttribute("content"));
|
||||
Browser.Equal("Immutable meta content", () => metaNoBindings.GetAttribute("content"));
|
||||
|
||||
// Change parameter of meta component
|
||||
var inputMetaBinding = Browser.FindElement(By.Id("input-meta-binding"));
|
||||
inputMetaBinding.Clear();
|
||||
inputMetaBinding.SendKeys("Updated meta content\n");
|
||||
|
||||
// Wait for meta tag to be recreated with new attributes
|
||||
metaWithBindings = WaitForNewElement(metaWithBindings, "meta-with-bindings");
|
||||
|
||||
// Validate new meta content attribute
|
||||
Browser.Equal("Updated meta content", () => metaWithBindings.GetAttribute("content"));
|
||||
|
||||
IWebElement WaitForNewElement(IWebElement existingElement, string id)
|
||||
{
|
||||
var newElement = existingElement;
|
||||
|
||||
Browser.NotEqual(existingElement, () => newElement = Browser.FindElement(By.Id(id)) ?? newElement);
|
||||
|
||||
return newElement;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReadUrlHashOnlyOnceConnected()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETests.Tests
|
||||
{
|
||||
public class HeadComponentsTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
||||
{
|
||||
public HeadComponentsTest(
|
||||
BrowserFixture browserFixture,
|
||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void InitializeAsyncCore()
|
||||
{
|
||||
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
|
||||
Browser.MountTestComponent<ModifyHeadComponent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Title_DoesChangeDocumentTitle()
|
||||
{
|
||||
var titleCount = 3;
|
||||
var titleButtonsById = Enumerable.Range(0, titleCount)
|
||||
.Select(i => (i, Browser.FindElement(By.Id($"button-title-{i}"))))
|
||||
.ToList();
|
||||
|
||||
Assert.All(titleButtonsById, buttonById =>
|
||||
{
|
||||
var (id, button) = buttonById;
|
||||
button.Click();
|
||||
|
||||
Browser.Equal($"Title {id}", () => Browser.Title);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Title_DeepestComponentHasPriority()
|
||||
{
|
||||
var nestedTitleButton = Browser.FindElement(By.Id("button-title-nested"));
|
||||
nestedTitleButton.Click();
|
||||
|
||||
Browser.Equal("Layer 4", () => Browser.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Meta_AddsAndRemovesElements()
|
||||
{
|
||||
var metaCount = 3;
|
||||
var metaButtonsById = Enumerable.Range(0, metaCount)
|
||||
.Select(i => (i, Browser.FindElement(By.Id($"button-meta-{i}"))))
|
||||
.ToList();
|
||||
|
||||
// Validate adding elements
|
||||
Assert.All(metaButtonsById, buttonById =>
|
||||
{
|
||||
var (id, button) = buttonById;
|
||||
button.Click();
|
||||
|
||||
Browser.Exists(By.Id($"Meta {id}"));
|
||||
});
|
||||
|
||||
// Validate removing elements
|
||||
Assert.All(metaButtonsById, buttonById =>
|
||||
{
|
||||
var (id, button) = buttonById;
|
||||
button.Click();
|
||||
|
||||
Browser.DoesNotExist(By.Id($"Meta {id}"));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Meta_UpdatesSameElementWhenComponentPropertyChanged()
|
||||
{
|
||||
var metaAttributeInput1 = Browser.FindElement(By.Id("meta-attr-input-1"));
|
||||
var metaAttributeInput2 = Browser.FindElement(By.Id("meta-attr-input-2"));
|
||||
var metaElement = FindMetaElement();
|
||||
|
||||
// Validate initial attribute values
|
||||
Browser.Equal("First attribute", () => metaElement.GetAttribute("attr1"));
|
||||
Browser.Equal("Second attribute", () => metaElement.GetAttribute("attr2"));
|
||||
|
||||
// Update the first parameter of the component
|
||||
metaAttributeInput1.Clear();
|
||||
metaAttributeInput1.SendKeys("hello\n");
|
||||
metaElement = FindMetaElement();
|
||||
|
||||
// Validate first attribute updated
|
||||
Browser.Equal("hello", () => metaElement.GetAttribute("attr1"));
|
||||
Browser.Equal("Second attribute", () => metaElement.GetAttribute("attr2"));
|
||||
|
||||
// Update the second parameter of the component
|
||||
metaAttributeInput2.Clear();
|
||||
metaAttributeInput2.SendKeys("world\n");
|
||||
metaElement = FindMetaElement();
|
||||
|
||||
// Validate second attribute updated
|
||||
Browser.Equal("hello", () => metaElement.GetAttribute("attr1"));
|
||||
Browser.Equal("world", () => metaElement.GetAttribute("attr2"));
|
||||
|
||||
IWebElement FindMetaElement() => Browser.FindElements(By.Id("meta-with-bindings")).Single();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Link_AddsAndRemovesElements()
|
||||
{
|
||||
var linkCount = 3;
|
||||
var linkButtonsById = Enumerable.Range(0, linkCount)
|
||||
.Select(i => (i, Browser.FindElement(By.Id($"button-link-{i}"))))
|
||||
.ToList();
|
||||
|
||||
// Validate adding elements
|
||||
Assert.All(linkButtonsById, buttonById =>
|
||||
{
|
||||
var (id, button) = buttonById;
|
||||
button.Click();
|
||||
|
||||
Browser.Exists(By.Id($"Link {id}"));
|
||||
});
|
||||
|
||||
// Validate removing elements
|
||||
Assert.All(linkButtonsById, buttonById =>
|
||||
{
|
||||
var (id, button) = buttonById;
|
||||
button.Click();
|
||||
|
||||
Browser.DoesNotExist(By.Id($"Link {id}"));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Link_UpdatesSameElementWhenComponentPropertyChanged()
|
||||
{
|
||||
var linkAttributeInput1 = Browser.FindElement(By.Id("link-attr-input-1"));
|
||||
var linkAttributeInput2 = Browser.FindElement(By.Id("link-attr-input-2"));
|
||||
var linkElement = FindLinkElement();
|
||||
|
||||
// Validate initial attribute values
|
||||
Browser.Equal("First attribute", () => linkElement.GetAttribute("attr1"));
|
||||
Browser.Equal("Second attribute", () => linkElement.GetAttribute("attr2"));
|
||||
|
||||
// Update the first parameter of the component
|
||||
linkAttributeInput1.Clear();
|
||||
linkAttributeInput1.SendKeys("hello\n");
|
||||
linkElement = FindLinkElement();
|
||||
|
||||
// Validate first attribute updated
|
||||
Browser.Equal("hello", () => linkElement.GetAttribute("attr1"));
|
||||
Browser.Equal("Second attribute", () => linkElement.GetAttribute("attr2"));
|
||||
|
||||
// Update the second parameter of the component
|
||||
linkAttributeInput2.Clear();
|
||||
linkAttributeInput2.SendKeys("world\n");
|
||||
linkElement = FindLinkElement();
|
||||
|
||||
// Validate second attribute updated
|
||||
Browser.Equal("hello", () => linkElement.GetAttribute("attr1"));
|
||||
Browser.Equal("world", () => linkElement.GetAttribute("attr2"));
|
||||
|
||||
IWebElement FindLinkElement() => Browser.FindElements(By.Id("link-with-bindings")).Single();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,6 +111,21 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Assert.True(renderedElement.Displayed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsErrorForUnavailableAssemblies()
|
||||
{
|
||||
// Navigate to a page with lazy loaded assemblies for the first time
|
||||
SetUrlViaPushState("/Other");
|
||||
var app = Browser.MountTestComponent<TestRouterWithLazyAssembly>();
|
||||
|
||||
// Should've thrown an error for unhandled error
|
||||
var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
|
||||
Assert.NotNull(errorUiElem);
|
||||
|
||||
|
||||
AssertLogContainsCriticalMessages("DoesNotExist.dll must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.");
|
||||
}
|
||||
|
||||
private string SetUrlViaPushState(string relativeUri)
|
||||
{
|
||||
var pathBaseWithoutHash = ServerPathBase.Split('#')[0];
|
||||
|
|
@ -145,5 +160,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
void AssertLogContainsCriticalMessages(params string[] messages)
|
||||
{
|
||||
var log = Browser.Manage().Logs.GetLog(LogType.Browser);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
Assert.Contains(log, entry =>
|
||||
{
|
||||
return entry.Level == LogLevel.Severe
|
||||
&& entry.Message.Contains(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
<option value="BasicTestApp.LoggingComponent">Logging</option>
|
||||
<option value="BasicTestApp.LongRunningInterop">Long running interop</option>
|
||||
<option value="BasicTestApp.MarkupBlockComponent">Markup blocks</option>
|
||||
<option value="BasicTestApp.ModifyHeadComponent">Modify head</option>
|
||||
<option value="BasicTestApp.MouseEventComponent">Mouse events</option>
|
||||
<option value="BasicTestApp.MovingCheckboxesComponent">Moving checkboxes diff case</option>
|
||||
<option value="BasicTestApp.MultipleChildContent">Multiple child content</option>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
@using Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
|
||||
<p>
|
||||
Multiple title elements:<br />
|
||||
|
||||
@for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var titleId = i;
|
||||
|
||||
<button id="button-title-@i" @onclick="() => SetSelectedTitle(titleId)">
|
||||
Title @titleId
|
||||
</button>
|
||||
|
||||
if (selectedTitle == titleId)
|
||||
{
|
||||
<Title Value="@($"Title {titleId}")" />
|
||||
}
|
||||
}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Multiple meta elements:<br />
|
||||
|
||||
@for (int i = 0; i < metas.Length; i++)
|
||||
{
|
||||
var metaId = i;
|
||||
|
||||
<button id="button-meta-@i" @onclick="() => Toggle(metas, metaId)">
|
||||
@GetToggleString(metas[metaId]) meta @metaId</button>
|
||||
|
||||
if (metas[metaId])
|
||||
{
|
||||
<Meta id="@($"Meta {metaId}")" />
|
||||
}
|
||||
}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Multiple link elements:<br />
|
||||
|
||||
@for (int i = 0; i < links.Length; i++)
|
||||
{
|
||||
var linkId = i;
|
||||
|
||||
<button id="button-link-@i" @onclick="() => Toggle(links, linkId)">
|
||||
@GetToggleString(links[linkId]) link @linkId</button>
|
||||
|
||||
if (links[linkId])
|
||||
{
|
||||
<Link id="@($"Link {linkId}")" />
|
||||
}
|
||||
}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Nested title elements:<br />
|
||||
|
||||
<button id="button-title-nested" @onclick="() => SetSelectedTitle(3)">
|
||||
Nested titles
|
||||
</button>
|
||||
|
||||
@if (selectedTitle == 3)
|
||||
{
|
||||
<div>
|
||||
<Title Value="Layer 1" />
|
||||
<div>
|
||||
<Title Value="Layer 2" />
|
||||
<div>
|
||||
<Title Value="Layer 3" />
|
||||
<div>
|
||||
<Title Value="Layer 4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Meta elements w/ bindings:<br />
|
||||
<input id="meta-attr-input-1" @bind="@metaAttribute1" placeholder="Attribute 1" /><br />
|
||||
<input id="meta-attr-input-2" @bind="@metaAttribute2" placeholder="Attribute 2" /><br />
|
||||
<Meta id="meta-with-bindings" attr1="@metaAttribute1" attr2="@metaAttribute2" />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Link elements w/ bindings:<br />
|
||||
<input id="link-attr-input-1" @bind="@linkAttribute1" placeholder="Attribute 1" /><br />
|
||||
<input id="link-attr-input-2" @bind="@linkAttribute2" placeholder="Attribute 2" /><br />
|
||||
<Link id="link-with-bindings" attr1="@linkAttribute1" attr2="@linkAttribute2" />
|
||||
</p>
|
||||
|
||||
@code {
|
||||
private readonly bool[] metas = Enumerable.Repeat(false, 3).ToArray();
|
||||
private readonly bool[] links = Enumerable.Repeat(false, 3).ToArray();
|
||||
|
||||
private int selectedTitle = -1;
|
||||
|
||||
private string metaAttribute1 = "First attribute";
|
||||
private string metaAttribute2 = "Second attribute";
|
||||
|
||||
private string linkAttribute1 = "First attribute";
|
||||
private string linkAttribute2 = "Second attribute";
|
||||
|
||||
private void Toggle(bool[] states, int index)
|
||||
{
|
||||
states[index] = !states[index];
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void SetSelectedTitle(int title)
|
||||
{
|
||||
selectedTitle = title;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private string GetToggleString(bool b)
|
||||
=> b ? "Disable" : "Enable";
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
@page "/prerendered-head"
|
||||
|
||||
@using Microsoft.AspNetCore.Components.Web.Extensions.Head
|
||||
@using Microsoft.JSInterop
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<p>
|
||||
This component demonstrates that head components (i.e. Title, Meta, etc.) can take effect during prerendering
|
||||
and become updatable when the circuit connects.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Title:<br />
|
||||
<input id="title-input" @bind="title" placeholder="Set the title" />
|
||||
<Title Value=@title />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Meta:<br />
|
||||
<input id="input-meta-binding" @bind="metaContent" placeholder="Set the meta content" />
|
||||
<Meta id="meta-with-bindings" content="@metaContent" />
|
||||
</p>
|
||||
|
||||
<Meta id="meta-no-bindings" content="Immutable meta content" />
|
||||
|
||||
@code {
|
||||
private string title = "Initial title";
|
||||
private string metaContent = "Initial meta content";
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using System.Reflection
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||
|
||||
@inject LazyAssemblyLoader lazyLoader
|
||||
|
||||
|
|
@ -31,25 +31,24 @@
|
|||
|
||||
private async Task LoadAssemblies(string uri)
|
||||
{
|
||||
try
|
||||
if (uri.EndsWith("WithLazyAssembly"))
|
||||
{
|
||||
if (uri.EndsWith("WithLazyAssembly"))
|
||||
{
|
||||
Console.WriteLine($"Loading assemblies for WithLazyAssembly...");
|
||||
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "Newtonsoft.Json.dll" });
|
||||
lazyLoadedAssemblies.AddRange(assemblies);
|
||||
}
|
||||
|
||||
if (uri.EndsWith("WithLazyLoadedRoutes"))
|
||||
{
|
||||
Console.WriteLine($"Loading assemblies for WithLazyLoadedRoutes...");
|
||||
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "LazyTestContentPackage.dll" });
|
||||
lazyLoadedAssemblies.AddRange(assemblies);
|
||||
}
|
||||
Console.WriteLine($"Loading assemblies for WithLazyAssembly...");
|
||||
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "Newtonsoft.Json.dll" });
|
||||
lazyLoadedAssemblies.AddRange(assemblies);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
if (uri.EndsWith("WithLazyLoadedRoutes"))
|
||||
{
|
||||
Console.WriteLine($"Error when loading assemblies: {e}");
|
||||
Console.WriteLine($"Loading assemblies for WithLazyLoadedRoutes...");
|
||||
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "LazyTestContentPackage.dll" });
|
||||
lazyLoadedAssemblies.AddRange(assemblies);
|
||||
}
|
||||
|
||||
if (uri.EndsWith("Other")) {
|
||||
Console.WriteLine($"Loading assemblies for Other...");
|
||||
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "DoesNotExist.dll" });
|
||||
lazyLoadedAssemblies.AddRange(assemblies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@
|
|||
</script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
|
||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
||||
|
||||
<!-- Used by ExternalContentPackage -->
|
||||
<script src="_content/TestContentPackage/prompt.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
<button id="load-boot-script" onclick="start()">Load boot script</button>
|
||||
|
||||
<script src="_framework/blazor.server.js" autostart="false"></script>
|
||||
|
||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
||||
|
||||
<script>
|
||||
// Used by InteropOnInitializationComponent
|
||||
function setElementValue(element, newValue) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@
|
|||
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
|
||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
||||
|
||||
<!-- Used by ExternalContentPackage -->
|
||||
<script src="_content/TestContentPackage/prompt.js"></script>
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace TestServer
|
||||
{
|
||||
|
|
|
|||
|
|
@ -473,6 +473,36 @@ namespace Microsoft.AspNetCore.Builder
|
|||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
MapDynamicControllerRoute<TTransformer>(endpoints, pattern, state: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will
|
||||
/// attempt to select a controller action using the route values produced by <typeparamref name="TTransformer"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The URL pattern of the route.</param>
|
||||
/// <param name="state">A state object to provide to the <typeparamref name="TTransformer" /> instance.</param>
|
||||
/// <typeparam name="TTransformer">The type of a <see cref="DynamicRouteValueTransformer"/>.</typeparam>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method allows the registration of a <see cref="RouteEndpoint"/> and <see cref="DynamicRouteValueTransformer"/>
|
||||
/// that combine to dynamically select a controller action using custom logic.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The instance of <typeparamref name="TTransformer"/> will be retrieved from the dependency injection container.
|
||||
/// Register <typeparamref name="TTransformer"/> as transient in <c>ConfigureServices</c>. Using the transient lifetime
|
||||
/// is required when using <paramref name="state" />.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapDynamicControllerRoute<TTransformer>(this IEndpointRouteBuilder endpoints, string pattern, object state)
|
||||
where TTransformer : DynamicRouteValueTransformer
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
EnsureControllerServices(endpoints);
|
||||
|
||||
// Called for side-effect to make sure that the data source is registered.
|
||||
|
|
@ -486,7 +516,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
})
|
||||
.Add(b =>
|
||||
{
|
||||
b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(typeof(TTransformer)));
|
||||
b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(typeof(TTransformer), state));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -519,4 +519,7 @@
|
|||
<data name="ValidationVisitor_ContainerCannotBeSpecified" xml:space="preserve">
|
||||
<value>A container cannot be specified when the ModelMetada is of kind '{0}'.</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="StateShouldBeNullForRouteValueTransformers" xml:space="preserve">
|
||||
<value>Transformer '{0}' was retrieved from dependency injection with a state value. State can only be specified when the dynamic route is mapped using MapDynamicControllerRoute's state argument together with transient lifetime transformer. Ensure that '{0}' doesn't set its own state and that the transformer is registered with a transient lifetime in dependency injection.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -97,13 +98,21 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
// no realistic way this could happen.
|
||||
var dynamicControllerMetadata = endpoint.Metadata.GetMetadata<DynamicControllerMetadata>();
|
||||
var transformerMetadata = endpoint.Metadata.GetMetadata<DynamicControllerRouteValueTransformerMetadata>();
|
||||
|
||||
DynamicRouteValueTransformer transformer = null;
|
||||
if (dynamicControllerMetadata != null)
|
||||
{
|
||||
dynamicValues = dynamicControllerMetadata.Values;
|
||||
}
|
||||
else if (transformerMetadata != null)
|
||||
{
|
||||
var transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
|
||||
transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
|
||||
if (transformer.State != null)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatStateShouldBeNullForRouteValueTransformers(transformerMetadata.SelectorType.Name));
|
||||
}
|
||||
transformer.State = transformerMetadata.State;
|
||||
|
||||
dynamicValues = await transformer.TransformAsync(httpContext, originalValues);
|
||||
}
|
||||
else
|
||||
|
|
@ -146,6 +155,16 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
}
|
||||
}
|
||||
|
||||
if (transformer != null)
|
||||
{
|
||||
endpoints = await transformer.FilterAsync(httpContext, values, endpoints);
|
||||
if (endpoints.Count == 0)
|
||||
{
|
||||
candidates.ReplaceEndpoint(i, null, null);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the route values
|
||||
candidates.ReplaceEndpoint(i, endpoint, values);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
{
|
||||
internal class DynamicControllerRouteValueTransformerMetadata : IDynamicEndpointMetadata
|
||||
{
|
||||
public DynamicControllerRouteValueTransformerMetadata(Type selectorType)
|
||||
public DynamicControllerRouteValueTransformerMetadata(Type selectorType, object state)
|
||||
{
|
||||
if (selectorType == null)
|
||||
{
|
||||
|
|
@ -23,10 +23,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
}
|
||||
|
||||
SelectorType = selectorType;
|
||||
State = state;
|
||||
}
|
||||
|
||||
public bool IsDynamic => true;
|
||||
|
||||
public Type SelectorType { get; }
|
||||
|
||||
public object State { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
|
@ -20,17 +21,40 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
/// <para>
|
||||
/// The route values returned from a <see cref="TransformAsync(HttpContext, RouteValueDictionary)"/> implementation
|
||||
/// will be used to select an action based on matching of the route values. All actions that match the route values
|
||||
/// will be considered as candidates, and may be further disambiguated by <see cref="IEndpointSelectorPolicy" />
|
||||
/// implementations such as <see cref="HttpMethodMatcherPolicy" />.
|
||||
/// will be considered as candidates, and may be further disambiguated by
|
||||
/// <see cref="FilterAsync(HttpContext, RouteValueDictionary, IReadOnlyList{Endpoint})" /> as well as
|
||||
/// <see cref="IEndpointSelectorPolicy" /> implementations such as <see cref="HttpMethodMatcherPolicy" />.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Operations on a <see cref="DynamicRouteValueTransformer" /> instance will be called for each dynamic endpoint
|
||||
/// in the following sequence:
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="State" /> is set</description></item>
|
||||
/// <item><description><see cref="TransformAsync(HttpContext, RouteValueDictionary)"/></description></item>
|
||||
/// <item><description><see cref="FilterAsync(HttpContext, RouteValueDictionary, IReadOnlyList{Endpoint})" /></description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// Implementations that are registered with the service collection as transient may safely use class
|
||||
/// members to persist state across these operations.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Implementations <see cref="DynamicRouteValueTransformer" /> should be registered with the service
|
||||
/// collection as type <see cref="DynamicRouteValueTransformer" />. Implementations can use any service
|
||||
/// lifetime.
|
||||
/// lifetime. Implementations that make use of <see cref="State" /> must be registered as transient.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class DynamicRouteValueTransformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a state value. An arbitrary value passed to the transformer from where it was registered.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations that make use of <see cref="State" /> must be registered as transient with the service
|
||||
/// collection.
|
||||
/// </remarks>
|
||||
public object State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a set of transformed route values that will be used to select an action.
|
||||
/// </summary>
|
||||
|
|
@ -38,5 +62,32 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
/// <param name="values">The route values associated with the current match. Implementations should not modify <paramref name="values"/>.</param>
|
||||
/// <returns>A task which asynchronously returns a set of route values.</returns>
|
||||
public abstract ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values);
|
||||
|
||||
/// <summary>
|
||||
/// Filters the set of endpoints that were chosen as a result of lookup based on the route values returned by
|
||||
/// <see cref="TransformAsync(HttpContext, RouteValueDictionary)" />.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext" /> associated with the current request.</param>
|
||||
/// <param name="values">The route values returned from <see cref="TransformAsync(HttpContext, RouteValueDictionary)" />.</param>
|
||||
/// <param name="endpoints">
|
||||
/// The endpoints that were chosen as a result of lookup based on the route values returned by
|
||||
/// <see cref="TransformAsync(HttpContext, RouteValueDictionary)" />.
|
||||
/// </param>
|
||||
/// <returns>Asynchronously returns a list of endpoints to apply to the matches collection.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementations of <see cref="FilterAsync(HttpContext, RouteValueDictionary, IReadOnlyList{Endpoint})" /> may further
|
||||
/// refine the list of endpoints chosen based on route value matching by returning a new list of endpoints based on
|
||||
/// <paramref name="endpoints" />.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="FilterAsync(HttpContext, RouteValueDictionary, IReadOnlyList{Endpoint})" /> will not be called in the case
|
||||
/// where zero endpoints were matched based on route values.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public virtual ValueTask<IReadOnlyList<Endpoint>> FilterAsync(HttpContext httpContext, RouteValueDictionary values, IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
return new ValueTask<IReadOnlyList<Endpoint>>(endpoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
_ => Task.CompletedTask,
|
||||
new EndpointMetadataCollection(new object[]
|
||||
{
|
||||
new DynamicControllerRouteValueTransformerMetadata(typeof(CustomTransformer)),
|
||||
new DynamicControllerRouteValueTransformerMetadata(typeof(CustomTransformer), State),
|
||||
}),
|
||||
"dynamic");
|
||||
|
||||
|
|
@ -68,10 +68,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddRouting();
|
||||
services.AddScoped<CustomTransformer>(s =>
|
||||
services.AddTransient<CustomTransformer>(s =>
|
||||
{
|
||||
var transformer = new CustomTransformer();
|
||||
transformer.Transform = (c, values) => Transform(c, values);
|
||||
transformer.Transform = (c, values, state) => Transform(c, values, state);
|
||||
transformer.Filter = (c, values, state, candidates) => Filter(c, values, state, candidates);
|
||||
return transformer;
|
||||
});
|
||||
Services = services.BuildServiceProvider();
|
||||
|
|
@ -91,7 +92,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
private IServiceProvider Services { get; }
|
||||
|
||||
private Func<HttpContext, RouteValueDictionary, ValueTask<RouteValueDictionary>> Transform { get; set; }
|
||||
private Func<HttpContext, RouteValueDictionary, object, ValueTask<RouteValueDictionary>> Transform { get; set; }
|
||||
|
||||
private Func<HttpContext, RouteValueDictionary, object, IReadOnlyList<Endpoint>, ValueTask<IReadOnlyList<Endpoint>>> Filter { get; set; } = (_, __, ___, e) => new ValueTask<IReadOnlyList<Endpoint>>(e);
|
||||
|
||||
private object State { get; } = new object();
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_NoMatch()
|
||||
|
|
@ -106,7 +111,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
candidates.SetValidity(0, false);
|
||||
|
||||
Transform = (c, values) =>
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
};
|
||||
|
|
@ -135,7 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values) =>
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary());
|
||||
};
|
||||
|
|
@ -166,7 +171,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values) =>
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
|
|
@ -200,6 +205,37 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
Assert.True(candidates.IsValidCandidate(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_ThrowsForTransformerWithInvalidLifetime()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
||||
|
||||
var endpoints = new[] { DynamicEndpoint, };
|
||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||
var scores = new[] { 0, };
|
||||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
controller = "Home",
|
||||
action = "Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
var httpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = new ServiceCollection().AddScoped(sp => new CustomTransformer { State = "Invalid" }).BuildServiceProvider(),
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => policy.ApplyAsync(httpContext, candidates));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
|
||||
{
|
||||
|
|
@ -212,12 +248,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values) =>
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
controller = "Home",
|
||||
action = "Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
@ -242,15 +279,164 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
{
|
||||
Assert.Equal("controller", kvp.Key);
|
||||
Assert.Equal("Home", kvp.Value);
|
||||
},
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("slug", kvp.Key);
|
||||
Assert.Equal("test", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("state", kvp.Key);
|
||||
Assert.Same(State, kvp.Value);
|
||||
});
|
||||
Assert.True(candidates.IsValidCandidate(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_CanDiscardFoundEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
||||
|
||||
var endpoints = new[] { DynamicEndpoint, };
|
||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||
var scores = new[] { 0, };
|
||||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
controller = "Home",
|
||||
action = "Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
Filter = (c, values, state, endpoints) =>
|
||||
{
|
||||
return new ValueTask<IReadOnlyList<Endpoint>>(Array.Empty<Endpoint>());
|
||||
};
|
||||
|
||||
var httpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = Services,
|
||||
};
|
||||
|
||||
// Act
|
||||
await policy.ApplyAsync(httpContext, candidates);
|
||||
|
||||
// Assert
|
||||
Assert.False(candidates.IsValidCandidate(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_CanReplaceFoundEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
||||
|
||||
var endpoints = new[] { DynamicEndpoint, };
|
||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||
var scores = new[] { 0, };
|
||||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
controller = "Home",
|
||||
action = "Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
Filter = (c, values, state, endpoints) => new ValueTask<IReadOnlyList<Endpoint>>(new[]
|
||||
{
|
||||
new Endpoint((ctx) => Task.CompletedTask, new EndpointMetadataCollection(Array.Empty<object>()), "ReplacedEndpoint")
|
||||
});
|
||||
|
||||
var httpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = Services,
|
||||
};
|
||||
|
||||
// Act
|
||||
await policy.ApplyAsync(httpContext, candidates);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
candidates[0].Values.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("action", kvp.Key);
|
||||
Assert.Equal("Index", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("controller", kvp.Key);
|
||||
Assert.Equal("Home", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("slug", kvp.Key);
|
||||
Assert.Equal("test", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("state", kvp.Key);
|
||||
Assert.Same(State, kvp.Value);
|
||||
});
|
||||
Assert.Equal("ReplacedEndpoint", candidates[0].Endpoint.DisplayName);
|
||||
Assert.True(candidates.IsValidCandidate(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
|
||||
|
||||
var endpoints = new[] { DynamicEndpoint, };
|
||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||
var scores = new[] { 0, };
|
||||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
controller = "Home",
|
||||
action = "Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
Filter = (c, values, state, endpoints) => new ValueTask<IReadOnlyList<Endpoint>>(new[]
|
||||
{
|
||||
ControllerEndpoints[1], ControllerEndpoints[2]
|
||||
});
|
||||
|
||||
var httpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = Services,
|
||||
};
|
||||
|
||||
// Act
|
||||
await policy.ApplyAsync(httpContext, candidates);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, candidates.Count);
|
||||
Assert.True(candidates.IsValidCandidate(0));
|
||||
Assert.True(candidates.IsValidCandidate(1));
|
||||
Assert.Same(ControllerEndpoints[1], candidates[0].Endpoint);
|
||||
Assert.Same(ControllerEndpoints[2], candidates[1].Endpoint);
|
||||
}
|
||||
|
||||
private class TestDynamicControllerEndpointSelector : DynamicControllerEndpointSelector
|
||||
{
|
||||
public TestDynamicControllerEndpointSelector(EndpointDataSource dataSource)
|
||||
|
|
@ -261,11 +447,18 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
private class CustomTransformer : DynamicRouteValueTransformer
|
||||
{
|
||||
public Func<HttpContext, RouteValueDictionary, ValueTask<RouteValueDictionary>> Transform { get; set; }
|
||||
public Func<HttpContext, RouteValueDictionary, object, ValueTask<RouteValueDictionary>> Transform { get; set; }
|
||||
|
||||
public Func<HttpContext, RouteValueDictionary, object, IReadOnlyList<Endpoint>, ValueTask<IReadOnlyList<Endpoint>>> Filter { get; set; }
|
||||
|
||||
public override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
|
||||
{
|
||||
return Transform(httpContext, values);
|
||||
return Transform(httpContext, values, State);
|
||||
}
|
||||
|
||||
public override ValueTask<IReadOnlyList<Endpoint>> FilterAsync(HttpContext httpContext, RouteValueDictionary values, IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
return Filter(httpContext, values, State, endpoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -299,6 +299,30 @@ namespace Microsoft.AspNetCore.Builder
|
|||
/// </remarks>
|
||||
public static void MapDynamicPageRoute<TTransformer>(this IEndpointRouteBuilder endpoints, string pattern)
|
||||
where TTransformer : DynamicRouteValueTransformer
|
||||
{
|
||||
MapDynamicPageRoute<TTransformer>(endpoints, pattern, state: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will
|
||||
/// attempt to select a page using the route values produced by <typeparamref name="TTransformer"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The URL pattern of the route.</param>
|
||||
/// <param name="state">A state object to provide to the <typeparamref name="TTransformer" /> instance.</param>
|
||||
/// <typeparam name="TTransformer">The type of a <see cref="DynamicRouteValueTransformer"/>.</typeparam>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method allows the registration of a <see cref="RouteEndpoint"/> and <see cref="DynamicRouteValueTransformer"/>
|
||||
/// that combine to dynamically select a page using custom logic.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The instance of <typeparamref name="TTransformer"/> will be retrieved from the dependency injection container.
|
||||
/// Register <typeparamref name="TTransformer"/> with the desired service lifetime in <c>ConfigureServices</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static void MapDynamicPageRoute<TTransformer>(this IEndpointRouteBuilder endpoints, string pattern, object state)
|
||||
where TTransformer : DynamicRouteValueTransformer
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
|
|
@ -316,14 +340,14 @@ namespace Microsoft.AspNetCore.Builder
|
|||
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
|
||||
|
||||
endpoints.Map(
|
||||
pattern,
|
||||
pattern,
|
||||
context =>
|
||||
{
|
||||
throw new InvalidOperationException("This endpoint is not expected to be executed directly.");
|
||||
})
|
||||
.Add(b =>
|
||||
{
|
||||
b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(typeof(TTransformer)));
|
||||
b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(typeof(TTransformer), state));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -105,13 +105,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
// no realistic way this could happen.
|
||||
var dynamicPageMetadata = endpoint.Metadata.GetMetadata<DynamicPageMetadata>();
|
||||
var transformerMetadata = endpoint.Metadata.GetMetadata<DynamicPageRouteValueTransformerMetadata>();
|
||||
DynamicRouteValueTransformer transformer = null;
|
||||
if (dynamicPageMetadata != null)
|
||||
{
|
||||
dynamicValues = dynamicPageMetadata.Values;
|
||||
}
|
||||
else if (transformerMetadata != null)
|
||||
{
|
||||
var transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
|
||||
transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
|
||||
if (transformer.State != null)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatStateShouldBeNullForRouteValueTransformers(transformerMetadata.SelectorType.Name));
|
||||
}
|
||||
transformer.State = transformerMetadata.State;
|
||||
dynamicValues = await transformer.TransformAsync(httpContext, originalValues);
|
||||
}
|
||||
else
|
||||
|
|
@ -154,6 +160,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
if (transformer != null)
|
||||
{
|
||||
endpoints = await transformer.FilterAsync(httpContext, values, endpoints);
|
||||
if (endpoints.Count == 0)
|
||||
{
|
||||
candidates.ReplaceEndpoint(i, null, null);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the route values
|
||||
candidates.ReplaceEndpoint(i, endpoint, values);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
{
|
||||
internal class DynamicPageRouteValueTransformerMetadata : IDynamicEndpointMetadata
|
||||
{
|
||||
public DynamicPageRouteValueTransformerMetadata(Type selectorType)
|
||||
public DynamicPageRouteValueTransformerMetadata(Type selectorType, object state)
|
||||
{
|
||||
if (selectorType == null)
|
||||
{
|
||||
|
|
@ -24,10 +24,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
}
|
||||
|
||||
SelectorType = selectorType;
|
||||
State = state;
|
||||
}
|
||||
|
||||
public bool IsDynamic => true;
|
||||
|
||||
public object State { get; }
|
||||
|
||||
public Type SelectorType { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,4 +153,7 @@
|
|||
<data name="InvalidActionDescriptorModelType" xml:space="preserve">
|
||||
<value>The model type for '{0}' is of type '{1}' which is not assignable to its declared model type '{2}'.</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="StateShouldBeNullForRouteValueTransformers" xml:space="preserve">
|
||||
<value>Transformer '{0}' was retrieved from dependency injection with a state value. State can only be specified when the dynamic route is mapped using MapDynamicPageRoute's state argument together with transient lifetime transformer. Ensure that '{0}' doesn't set its own state and that the transformer is registered with a transient lifetime in dependency injection.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
|
|
@ -30,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
{
|
||||
["page"] = "/Index",
|
||||
},
|
||||
DisplayName = "/Index",
|
||||
},
|
||||
new PageActionDescriptor()
|
||||
{
|
||||
|
|
@ -37,6 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
{
|
||||
["page"] = "/About",
|
||||
},
|
||||
DisplayName = "/About"
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
_ => Task.CompletedTask,
|
||||
new EndpointMetadataCollection(new object[]
|
||||
{
|
||||
new DynamicPageRouteValueTransformerMetadata(typeof(CustomTransformer)),
|
||||
new DynamicPageRouteValueTransformerMetadata(typeof(CustomTransformer), State),
|
||||
}),
|
||||
"dynamic");
|
||||
|
||||
|
|
@ -60,24 +60,38 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
var services = new ServiceCollection();
|
||||
services.AddRouting();
|
||||
services.AddScoped<CustomTransformer>(s =>
|
||||
services.AddTransient<CustomTransformer>(s =>
|
||||
{
|
||||
var transformer = new CustomTransformer();
|
||||
transformer.Transform = (c, values) => Transform(c, values);
|
||||
transformer.Transform = (c, values, state) => Transform(c, values, state);
|
||||
transformer.Filter = (c, values, state, endpoints) => Filter(c, values, state, endpoints);
|
||||
return transformer;
|
||||
});
|
||||
Services = services.BuildServiceProvider();
|
||||
|
||||
Comparer = Services.GetRequiredService<EndpointMetadataComparer>();
|
||||
|
||||
LoadedEndpoint = new Endpoint(_ => Task.CompletedTask, EndpointMetadataCollection.Empty, "Loaded");
|
||||
LoadedEndpoints = new[]
|
||||
{
|
||||
new Endpoint(_ => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test1"),
|
||||
new Endpoint(_ => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test2"),
|
||||
new Endpoint(_ => Task.CompletedTask, EndpointMetadataCollection.Empty, "ReplacedLoaded")
|
||||
};
|
||||
|
||||
var loader = new Mock<PageLoader>();
|
||||
loader
|
||||
.Setup(l => l.LoadAsync(It.IsAny<PageActionDescriptor>()))
|
||||
.Returns(Task.FromResult(new CompiledPageActionDescriptor() { Endpoint = LoadedEndpoint, }));
|
||||
.Returns<PageActionDescriptor>(descriptor => Task.FromResult(new CompiledPageActionDescriptor
|
||||
{
|
||||
Endpoint = descriptor.DisplayName switch
|
||||
{
|
||||
"/Index" => LoadedEndpoints[0],
|
||||
"/About" => LoadedEndpoints[1],
|
||||
"/ReplacedEndpoint" => LoadedEndpoints[2],
|
||||
_ => throw new InvalidOperationException($"Invalid endpoint '{descriptor.DisplayName}'.")
|
||||
}
|
||||
}));
|
||||
Loader = loader.Object;
|
||||
|
||||
}
|
||||
|
||||
private EndpointMetadataComparer Comparer { get; }
|
||||
|
|
@ -88,15 +102,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
private Endpoint DynamicEndpoint { get; }
|
||||
|
||||
private Endpoint LoadedEndpoint { get; }
|
||||
private Endpoint [] LoadedEndpoints { get; }
|
||||
|
||||
private PageLoader Loader { get; }
|
||||
|
||||
private DynamicPageEndpointSelector Selector { get; }
|
||||
|
||||
private object State { get; }
|
||||
|
||||
private IServiceProvider Services { get; }
|
||||
|
||||
private Func<HttpContext, RouteValueDictionary, ValueTask<RouteValueDictionary>> Transform { get; set; }
|
||||
private Func<HttpContext, RouteValueDictionary, object, ValueTask<RouteValueDictionary>> Transform { get; set; }
|
||||
|
||||
private Func<HttpContext, RouteValueDictionary, object, IReadOnlyList<Endpoint>, ValueTask<IReadOnlyList<Endpoint>>> Filter { get; set; } = (_, __, ___, e) => new ValueTask<IReadOnlyList<Endpoint>>(e);
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_NoMatch()
|
||||
|
|
@ -111,7 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
candidates.SetValidity(0, false);
|
||||
|
||||
Transform = (c, values) =>
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
};
|
||||
|
|
@ -140,7 +158,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values) =>
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary());
|
||||
};
|
||||
|
|
@ -171,7 +189,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values) =>
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
|
|
@ -188,7 +206,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
await policy.ApplyAsync(httpContext, candidates);
|
||||
|
||||
// Assert
|
||||
Assert.Same(LoadedEndpoint, candidates[0].Endpoint);
|
||||
Assert.Same(LoadedEndpoints[0], candidates[0].Endpoint);
|
||||
Assert.Collection(
|
||||
candidates[0].Values.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
|
|
@ -211,11 +229,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values) =>
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
page = "/Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
@ -228,7 +247,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
await policy.ApplyAsync(httpContext, candidates);
|
||||
|
||||
// Assert
|
||||
Assert.Same(LoadedEndpoint, candidates[0].Endpoint);
|
||||
Assert.Same(LoadedEndpoints[0], candidates[0].Endpoint);
|
||||
Assert.Collection(
|
||||
candidates[0].Values.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
|
|
@ -240,10 +259,185 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
{
|
||||
Assert.Equal("slug", kvp.Key);
|
||||
Assert.Equal("test", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("state", kvp.Key);
|
||||
Assert.Same(State, kvp.Value);
|
||||
});
|
||||
Assert.True(candidates.IsValidCandidate(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_Throws_ForTransformersWithInvalidLifetime()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
||||
|
||||
var endpoints = new[] { DynamicEndpoint, };
|
||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||
var scores = new[] { 0, };
|
||||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
page = "/Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
var httpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = new ServiceCollection().AddScoped(sp => new CustomTransformer() { State = "Invalid" }).BuildServiceProvider()
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => policy.ApplyAsync(httpContext, candidates));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_CanDiscardFoundEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
||||
|
||||
var endpoints = new[] { DynamicEndpoint, };
|
||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||
var scores = new[] { 0, };
|
||||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
page = "/Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
Filter = (c, values, state, endpoints) =>
|
||||
{
|
||||
return new ValueTask<IReadOnlyList<Endpoint>>(Array.Empty<Endpoint>());
|
||||
};
|
||||
|
||||
var httpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = Services,
|
||||
};
|
||||
|
||||
// Act
|
||||
await policy.ApplyAsync(httpContext, candidates);
|
||||
|
||||
// Assert
|
||||
Assert.False(candidates.IsValidCandidate(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_CanReplaceFoundEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
||||
|
||||
var endpoints = new[] { DynamicEndpoint, };
|
||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||
var scores = new[] { 0, };
|
||||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
page = "/Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
Filter = (c, values, state, endpoints) => new ValueTask<IReadOnlyList<Endpoint>>(new[]
|
||||
{
|
||||
new Endpoint((ctx) => Task.CompletedTask, new EndpointMetadataCollection(new PageActionDescriptor()
|
||||
{
|
||||
DisplayName = "/ReplacedEndpoint",
|
||||
}), "ReplacedEndpoint")
|
||||
});
|
||||
|
||||
var httpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = Services,
|
||||
};
|
||||
|
||||
// Act
|
||||
await policy.ApplyAsync(httpContext, candidates);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, candidates.Count);
|
||||
Assert.Collection(
|
||||
candidates[0].Values.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("page", kvp.Key);
|
||||
Assert.Equal("/Index", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("slug", kvp.Key);
|
||||
Assert.Equal("test", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("state", kvp.Key);
|
||||
Assert.Same(State, kvp.Value);
|
||||
});
|
||||
Assert.Equal("ReplacedLoaded", candidates[0].Endpoint.DisplayName);
|
||||
Assert.True(candidates.IsValidCandidate(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer);
|
||||
|
||||
var endpoints = new[] { DynamicEndpoint, };
|
||||
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
|
||||
var scores = new[] { 0, };
|
||||
|
||||
var candidates = new CandidateSet(endpoints, values, scores);
|
||||
|
||||
Transform = (c, values, state) =>
|
||||
{
|
||||
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
|
||||
{
|
||||
page = "/Index",
|
||||
state
|
||||
}));
|
||||
};
|
||||
|
||||
Filter = (c, values, state, endpoints) => new ValueTask<IReadOnlyList<Endpoint>>(new[]
|
||||
{
|
||||
PageEndpoints[0], PageEndpoints[1]
|
||||
});
|
||||
|
||||
var httpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = Services,
|
||||
};
|
||||
|
||||
// Act
|
||||
await policy.ApplyAsync(httpContext, candidates);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, candidates.Count);
|
||||
Assert.True(candidates.IsValidCandidate(0));
|
||||
Assert.True(candidates.IsValidCandidate(1));
|
||||
Assert.Same(LoadedEndpoints[0], candidates[0].Endpoint);
|
||||
Assert.Same(LoadedEndpoints[1], candidates[1].Endpoint);
|
||||
}
|
||||
|
||||
private class TestDynamicPageEndpointSelector : DynamicPageEndpointSelector
|
||||
{
|
||||
public TestDynamicPageEndpointSelector(EndpointDataSource dataSource)
|
||||
|
|
@ -254,11 +448,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
private class CustomTransformer : DynamicRouteValueTransformer
|
||||
{
|
||||
public Func<HttpContext, RouteValueDictionary, ValueTask<RouteValueDictionary>> Transform { get; set; }
|
||||
public Func<HttpContext, RouteValueDictionary, object, ValueTask<RouteValueDictionary>> Transform { get; set; }
|
||||
|
||||
public Func<HttpContext, RouteValueDictionary, object, IReadOnlyList<Endpoint>, ValueTask<IReadOnlyList<Endpoint>>> Filter { get; set; }
|
||||
|
||||
public override ValueTask<IReadOnlyList<Endpoint>> FilterAsync(HttpContext httpContext, RouteValueDictionary values, IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
return Filter(httpContext, values, State, endpoints);
|
||||
}
|
||||
|
||||
public override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
|
||||
{
|
||||
return Transform(httpContext, values);
|
||||
return Transform(httpContext, values, State);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.Linq;
|
||||
|
|
@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public async Task DynamicController_CanSelectControllerInArea()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/dynamic/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
|
||||
var url = "http://localhost/v1/dynamic/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Act
|
||||
|
|
@ -71,11 +71,25 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("Hello from dynamic controller: /link_generation/dynamic/index", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicController_CanFilterResultsBasedOnState()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/v2/dynamic/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicController_CanSelectControllerInArea_WithActionConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/dynamic/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
|
||||
var url = "http://localhost/v1/dynamic/area%3Dadmin,controller%3Ddynamic,action%3Dindex";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
|
||||
// Act
|
||||
|
|
@ -91,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public async Task DynamicPage_CanSelectPage()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/dynamicpage/page%3D%2FDynamicPage";
|
||||
var url = "http://localhost/v1/dynamicpage/page%3D%2FDynamicPage";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Act
|
||||
|
|
@ -103,6 +117,21 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("Hello from dynamic page: /DynamicPage", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicPage_CanFilterBasedOnState()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/v2/dynamicpage/page%3D%2FDynamicPage";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppWithDynamicRouteAndMapRazorPages_CanRouteToRazorPage()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace RoutingWebSite
|
||||
{
|
||||
public class DynamicVersion
|
||||
{
|
||||
public string Version { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -21,7 +23,7 @@ namespace RoutingWebSite
|
|||
.AddNewtonsoftJson()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
||||
|
||||
services.AddSingleton<Transformer>();
|
||||
services.AddTransient<Transformer>();
|
||||
|
||||
// Used by some controllers defined in this project.
|
||||
services.Configure<RouteOptions>(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
|
||||
|
|
@ -32,8 +34,10 @@ namespace RoutingWebSite
|
|||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapDynamicControllerRoute<Transformer>("dynamic/{**slug}");
|
||||
endpoints.MapDynamicPageRoute<Transformer>("dynamicpage/{**slug}");
|
||||
endpoints.MapDynamicControllerRoute<Transformer>("v1/dynamic/{**slug}", new DynamicVersion { Version = "V1" });
|
||||
endpoints.MapDynamicControllerRoute<Transformer>("v2/dynamic/{**slug}", new DynamicVersion { Version = "V2" });
|
||||
endpoints.MapDynamicPageRoute<Transformer>("v1/dynamicpage/{**slug}", new DynamicVersion { Version = "V1" });
|
||||
endpoints.MapDynamicPageRoute<Transformer>("v2/dynamicpage/{**slug}", new DynamicVersion { Version = "V2" });
|
||||
|
||||
endpoints.MapControllerRoute("link", "link_generation/{controller}/{action}/{id?}");
|
||||
|
||||
|
|
@ -59,8 +63,21 @@ namespace RoutingWebSite
|
|||
results[split[0]] = split[1];
|
||||
}
|
||||
|
||||
results["version"] = ((DynamicVersion)State).Version;
|
||||
|
||||
return new ValueTask<RouteValueDictionary>(results);
|
||||
}
|
||||
|
||||
public override ValueTask<IReadOnlyList<Endpoint>> FilterAsync(HttpContext httpContext, RouteValueDictionary values, IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
var version = ((DynamicVersion)State).Version;
|
||||
if (version == "V2" && version == (string)values["version"])
|
||||
{
|
||||
// For v1 routes this transformer will work fine, for v2 routes, it will filter them.
|
||||
return new ValueTask<IReadOnlyList<Endpoint>>(Array.Empty<Endpoint>());
|
||||
}
|
||||
return base.FilterAsync(httpContext, values, endpoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace RoutingWebSite
|
|||
.AddMvc()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
||||
|
||||
services.AddSingleton<Transformer>();
|
||||
services.AddTransient<Transformer>();
|
||||
|
||||
// Used by some controllers defined in this project.
|
||||
services.Configure<RouteOptions>(options => options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
|
||||
|
|
|
|||
Loading…
Reference in New Issue