Merge branch 'master' into merge/release/5.0-preview8-to-master

This commit is contained in:
Safia Abdalla 2020-07-21 01:40:02 +00:00 committed by GitHub
commit 537ab96eb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 1563 additions and 272 deletions

66
.github/ISSUE_TEMPLATE/razor_tooling.md vendored Normal file
View File

@ -0,0 +1,66 @@
---
name: 🐞 Razor Tooling Bug report
about: Report an issue about something that is not working in the new Razor tooling
labels: area-razor.tooling, feature-razor.vs
---
<!--
More information on our issue management policies can be found here: https://aka.ms/aspnet/issue-policies
Please keep in mind that the GitHub issue tracker is not intended as a general support forum, but for reporting **non-security** bugs and feature requests.
If you believe you have an issue that affects the SECURITY of the platform, please do NOT create an issue and instead email your issue details to secure@microsoft.com. Your report may be eligible for our [bug bounty](https://www.microsoft.com/en-us/msrc/bounty-dot-net-core) but ONLY if it is reported through email.
For other types of questions, consider using [StackOverflow](https://stackoverflow.com).
-->
<!-- NOTE: This issue template is meant specifically to be used for issues with the new experimental Razor tooling experience provided in Visual Studio's Preview Feature pane -->
### Describe the bug
A clear and concise description of what the bug is.
### To Reproduce
<!--
We ❤ code! Point us to a minimalistic repro project hosted in a GitHub repo.
For a repro project, create a new ASP.NET Core project using the template of your your choice, apply the minimum required code to result in the issue you're observing.
We will close this issue if:
- the repro project you share with us is complex. We can't investigate custom projects, so don't point us to such, please.
- if we will not be able to repro the behavior you're reporting
-->
### Logs & Exceptions
Please collect the data below before reporting your issue to aid us in diagnosing the root cause.
#### Activity log
[Here](https://docs.microsoft.com/en-us/visualstudio/extensibility/how-to-use-the-activity-log?view=vs-2019#to-examine-the-activity-log) are the instructions on how to generate/acquire one.
#### Razor Language Server Client log
<!-- In Visual Studio's `Output` window, the drop-down contains a `Razor Language Server Client` item. Include that below. -->
<details>
<summary>Razor Language Server Client Log Output</summary>
Paste log output here
</details>
#### HTML Language Server Client log
<!-- In Visual Studio's `Output` window, the drop-down contains a `HtmlyLanguageClient` item. Include that below. -->
<details>
<summary>HTML Language Server Client Log Output</summary>
Paste log output here
</details>
### Further technical details
- VS version (Help => About Microsoft Visual Studio, i.e. 16.8.0 Preview 1 30313.27...). If in Codespaces there will be two versions (server and client), please provide both.
- Scenario (Local, LiveShare, Codespaces)
### Pre-requisite checklist
- [ ] Steps to reproduce the issue
- [ ] Visual Studio Activity Log attached.
- [ ] Razor Language Server client logs included.
- [ ] HTML Language Server client logs included

View File

@ -1437,6 +1437,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWeb
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{6126DCE4-9692-4EE2-B240-C65743572995}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicTestApp", "src\Components\test\testassets\BasicTestApp\BasicTestApp.csproj", "{46FB7E93-1294-4068-B80A-D4864F78277A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComponentsApp.App", "src\Components\test\testassets\ComponentsApp.App\ComponentsApp.App.csproj", "{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComponentsApp.Server", "src\Components\test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{19974360-4A63-425A-94DB-C2C940A3A97A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyTestContentPackage", "src\Components\test\testassets\LazyTestContentPackage\LazyTestContentPackage.csproj", "{ADF9C126-F322-4E34-AFD3-E626A4487206}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestContentPackage", "src\Components\test\testassets\TestContentPackage\TestContentPackage.csproj", "{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components.TestServer", "src\Components\test\testassets\TestServer\Components.TestServer.csproj", "{8A59AF88-4A82-46ED-977D-D909001F8107}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -6787,6 +6801,78 @@ Global
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.Build.0 = Release|Any CPU {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.Build.0 = Release|Any CPU
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.ActiveCfg = Release|Any CPU {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.ActiveCfg = Release|Any CPU
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.Build.0 = Release|Any CPU {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.Build.0 = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x64.ActiveCfg = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x64.Build.0 = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x86.ActiveCfg = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x86.Build.0 = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|Any CPU.Build.0 = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|x64.ActiveCfg = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|x64.Build.0 = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|x86.ActiveCfg = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|x86.Build.0 = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|x64.ActiveCfg = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|x64.Build.0 = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|x86.ActiveCfg = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|x86.Build.0 = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|Any CPU.Build.0 = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|x64.ActiveCfg = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|x64.Build.0 = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|x86.ActiveCfg = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|x86.Build.0 = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|x64.ActiveCfg = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|x64.Build.0 = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|x86.ActiveCfg = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|x86.Build.0 = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|Any CPU.Build.0 = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|x64.ActiveCfg = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|x64.Build.0 = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|x86.ActiveCfg = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|x86.Build.0 = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|x64.ActiveCfg = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|x64.Build.0 = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|x86.ActiveCfg = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|x86.Build.0 = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|Any CPU.Build.0 = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|x64.ActiveCfg = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|x64.Build.0 = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|x86.ActiveCfg = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|x86.Build.0 = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|x64.Build.0 = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|x86.Build.0 = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|Any CPU.Build.0 = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|x64.ActiveCfg = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|x64.Build.0 = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|x86.ActiveCfg = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|x86.Build.0 = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|x64.Build.0 = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|x86.Build.0 = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|Any CPU.Build.0 = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x64.ActiveCfg = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x64.Build.0 = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x86.ActiveCfg = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -7507,6 +7593,13 @@ Global
{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} {83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
{525EBCB4-A870-470B-BC90-845306C337D1} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} {525EBCB4-A870-470B-BC90-845306C337D1} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
{175E5CD8-92D4-46BB-882E-3A930D3302D4} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} {175E5CD8-92D4-46BB-882E-3A930D3302D4} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
{6126DCE4-9692-4EE2-B240-C65743572995} = {0508E463-0269-40C9-B5C2-3B600FB2A28B}
{46FB7E93-1294-4068-B80A-D4864F78277A} = {6126DCE4-9692-4EE2-B240-C65743572995}
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16} = {6126DCE4-9692-4EE2-B240-C65743572995}
{19974360-4A63-425A-94DB-C2C940A3A97A} = {6126DCE4-9692-4EE2-B240-C65743572995}
{ADF9C126-F322-4E34-AFD3-E626A4487206} = {6126DCE4-9692-4EE2-B240-C65743572995}
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31} = {6126DCE4-9692-4EE2-B240-C65743572995}
{8A59AF88-4A82-46ED-977D-D909001F8107} = {6126DCE4-9692-4EE2-B240-C65743572995}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

View File

@ -133,10 +133,14 @@ docker run \
-t \ -t \
-e TF_BUILD \ -e TF_BUILD \
-e BUILD_NUMBER \ -e BUILD_NUMBER \
-e BUILD_BUILDID \
-e SYSTEM_TEAMPROJECT \
-e BUILD_BUILDNUMBER \ -e BUILD_BUILDNUMBER \
-e BUILD_REPOSITORY_URI \ -e BUILD_REPOSITORY_URI \
-e BUILD_SOURCEVERSION \ -e BUILD_SOURCEVERSION \
-e BUILD_SOURCEBRANCH \ -e BUILD_SOURCEBRANCH \
-e SYSTEM_DEFINITIONID \
-e SYSTEM_TEAMFOUNDATIONCOLLECTIONURI \
-e DOTNET_CLI_TELEMETRY_OPTOUT \ -e DOTNET_CLI_TELEMETRY_OPTOUT \
-e Configuration \ -e Configuration \
-v "$DIR:$DIR" \ -v "$DIR:$DIR" \

View File

@ -4,6 +4,22 @@ Building ASP.NET Core from source allows you to tweak and customize ASP.NET Core
See <https://github.com/dotnet/aspnetcore/labels/area-infrastructure> for known issues and to track ongoing work. See <https://github.com/dotnet/aspnetcore/labels/area-infrastructure> for known issues and to track ongoing work.
## Clone the source code
ASP.NET Core uses git submodules to include the source from a few other projects.
For a new copy of the project, run:
```ps1
git clone --recursive https://github.com/dotnet/aspnetcore
```
To update an existing copy, run:
```ps1
git submodule update --init --recursive
```
## Install pre-requisites ## Install pre-requisites
### Windows ### Windows
@ -22,7 +38,8 @@ Building ASP.NET Core on Windows requires:
However, any Visual Studio 2019 instance that meets the requirements should be fine. See [global.json](/global.json) However, any Visual Studio 2019 instance that meets the requirements should be fine. See [global.json](/global.json)
and [eng/scripts/vs.json](/eng/scripts/vs.json) for those requirements. By default, the script will install Visual Studio Enterprise Edition, however you can use a different edition by passing the `-Edition` flag. and [eng/scripts/vs.json](/eng/scripts/vs.json) for those requirements. By default, the script will install Visual Studio Enterprise Edition, however you can use a different edition by passing the `-Edition` flag.
* Git. <https://git-scm.org> * Git. <https://git-scm.org>
* NodeJS. LTS version of 10.14.2 or newer <https://nodejs.org> * NodeJS. LTS version of 10.14.2 or newer <https://nodejs.org>.
* Install yarn globally (`npm install -g yarn`)
* Java Development Kit 11 or newer. Either: * Java Development Kit 11 or newer. Either:
* OpenJDK <https://jdk.java.net/> * OpenJDK <https://jdk.java.net/>
* Oracle's JDK <https://www.oracle.com/technetwork/java/javase/downloads/index.html> * Oracle's JDK <https://www.oracle.com/technetwork/java/javase/downloads/index.html>
@ -52,22 +69,6 @@ Building ASP.NET Core on macOS or Linux requires:
* OpenJDK <https://jdk.java.net/> * OpenJDK <https://jdk.java.net/>
* Oracle's JDK <https://www.oracle.com/technetwork/java/javase/downloads/index.html> * Oracle's JDK <https://www.oracle.com/technetwork/java/javase/downloads/index.html>
## Clone the source code
ASP.NET Core uses git submodules to include the source from a few other projects.
For a new copy of the project, run:
```ps1
git clone --recursive https://github.com/dotnet/aspnetcore
```
To update an existing copy, run:
```ps1
git submodule update --init --recursive
```
**NOTE** some ISPs have been know to use web filtering software that has caused issues with git repository cloning, if you experience issues cloning this repo please review <https://help.github.com/en/github/authenticating-to-github/using-ssh-over-the-https-port> **NOTE** some ISPs have been know to use web filtering software that has caused issues with git repository cloning, if you experience issues cloning this repo please review <https://help.github.com/en/github/authenticating-to-github/using-ssh-over-the-https-port>
## Building in Visual Studio ## Building in Visual Studio
@ -86,6 +87,9 @@ Before opening our .sln/.slnf files in Visual Studio or VS Code, you need to per
> :bulb: Pro tip: you will also want to run this command after pulling large sets of changes. On the master > :bulb: Pro tip: you will also want to run this command after pulling large sets of changes. On the master
> branch, we regularly update the versions of .NET Core SDK required to build the repo. > branch, we regularly update the versions of .NET Core SDK required to build the repo.
> You will need to restart Visual Studio every time we update the .NET Core SDK. > You will need to restart Visual Studio every time we update the .NET Core SDK.
> To allow executing the setup script, you may need to update the execution policy on your machine.
You can do so by running the `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` command
in PowerShell. For more information on execution policies, you can read the [execution policy docs](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy).
2. Use the `startvs.cmd` script to open Visual Studio .sln/.slnf files. This script first sets the required 2. Use the `startvs.cmd` script to open Visual Studio .sln/.slnf files. This script first sets the required
environment variables. environment variables.

View File

@ -13,33 +13,33 @@
<Uri>https://github.com/dotnet/blazor</Uri> <Uri>https://github.com/dotnet/blazor</Uri>
<Sha>cc449601d638ffaab58ae9487f0fd010bb178a12</Sha> <Sha>cc449601d638ffaab58ae9487f0fd010bb178a12</Sha>
</Dependency> </Dependency>
<Dependency Name="dotnet-ef" Version="5.0.0-preview.8.20360.8"> <Dependency Name="dotnet-ef" Version="5.0.0-preview.8.20365.2">
<Uri>https://github.com/dotnet/efcore</Uri> <Uri>https://github.com/dotnet/efcore</Uri>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha> <Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.0-preview.8.20360.8"> <Dependency Name="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.0-preview.8.20365.2">
<Uri>https://github.com/dotnet/efcore</Uri> <Uri>https://github.com/dotnet/efcore</Uri>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha> <Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0-preview.8.20360.8"> <Dependency Name="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0-preview.8.20365.2">
<Uri>https://github.com/dotnet/efcore</Uri> <Uri>https://github.com/dotnet/efcore</Uri>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha> <Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0-preview.8.20360.8"> <Dependency Name="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0-preview.8.20365.2">
<Uri>https://github.com/dotnet/efcore</Uri> <Uri>https://github.com/dotnet/efcore</Uri>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha> <Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0-preview.8.20360.8"> <Dependency Name="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0-preview.8.20365.2">
<Uri>https://github.com/dotnet/efcore</Uri> <Uri>https://github.com/dotnet/efcore</Uri>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha> <Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0-preview.8.20360.8"> <Dependency Name="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0-preview.8.20365.2">
<Uri>https://github.com/dotnet/efcore</Uri> <Uri>https://github.com/dotnet/efcore</Uri>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha> <Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.EntityFrameworkCore" Version="5.0.0-preview.8.20360.8"> <Dependency Name="Microsoft.EntityFrameworkCore" Version="5.0.0-preview.8.20365.2">
<Uri>https://github.com/dotnet/efcore</Uri> <Uri>https://github.com/dotnet/efcore</Uri>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha> <Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</Dependency> </Dependency>
<Dependency Name="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0-preview.8.20361.2"> <Dependency Name="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0-preview.8.20361.2">
<Uri>https://github.com/dotnet/runtime</Uri> <Uri>https://github.com/dotnet/runtime</Uri>

View File

@ -9,14 +9,14 @@
<AspNetCoreMajorVersion>5</AspNetCoreMajorVersion> <AspNetCoreMajorVersion>5</AspNetCoreMajorVersion>
<AspNetCoreMinorVersion>0</AspNetCoreMinorVersion> <AspNetCoreMinorVersion>0</AspNetCoreMinorVersion>
<AspNetCorePatchVersion>0</AspNetCorePatchVersion> <AspNetCorePatchVersion>0</AspNetCorePatchVersion>
<PreReleaseVersionIteration>8</PreReleaseVersionIteration> <PreReleaseVersionIteration>1</PreReleaseVersionIteration>
<!-- <!--
When StabilizePackageVersion is set to 'true', this branch will produce stable outputs for 'Shipping' packages When StabilizePackageVersion is set to 'true', this branch will produce stable outputs for 'Shipping' packages
--> -->
<StabilizePackageVersion Condition="'$(StabilizePackageVersion)' == ''">false</StabilizePackageVersion> <StabilizePackageVersion Condition="'$(StabilizePackageVersion)' == ''">false</StabilizePackageVersion>
<DotNetFinalVersionKind Condition="'$(StabilizePackageVersion)' == 'true'">release</DotNetFinalVersionKind> <DotNetFinalVersionKind Condition="'$(StabilizePackageVersion)' == 'true'">release</DotNetFinalVersionKind>
<PreReleaseVersionLabel>preview</PreReleaseVersionLabel> <PreReleaseVersionLabel>rc</PreReleaseVersionLabel>
<PreReleaseBrandingLabel>Preview $(PreReleaseVersionIteration)</PreReleaseBrandingLabel> <PreReleaseBrandingLabel>RC $(PreReleaseVersionIteration)</PreReleaseBrandingLabel>
<IncludePreReleaseLabelInPackageVersion>true</IncludePreReleaseLabelInPackageVersion> <IncludePreReleaseLabelInPackageVersion>true</IncludePreReleaseLabelInPackageVersion>
<IncludePreReleaseLabelInPackageVersion Condition=" '$(DotNetFinalVersionKind)' == 'release' ">false</IncludePreReleaseLabelInPackageVersion> <IncludePreReleaseLabelInPackageVersion Condition=" '$(DotNetFinalVersionKind)' == 'release' ">false</IncludePreReleaseLabelInPackageVersion>
<AspNetCoreMajorMinorVersion>$(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion)</AspNetCoreMajorMinorVersion> <AspNetCoreMajorMinorVersion>$(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion)</AspNetCoreMajorMinorVersion>
@ -131,13 +131,13 @@
<!-- Packages from dotnet/blazor --> <!-- Packages from dotnet/blazor -->
<MicrosoftAspNetCoreComponentsWebAssemblyRuntimePackageVersion>3.2.0</MicrosoftAspNetCoreComponentsWebAssemblyRuntimePackageVersion> <MicrosoftAspNetCoreComponentsWebAssemblyRuntimePackageVersion>3.2.0</MicrosoftAspNetCoreComponentsWebAssemblyRuntimePackageVersion>
<!-- Packages from dotnet/efcore --> <!-- Packages from dotnet/efcore -->
<dotnetefPackageVersion>5.0.0-preview.8.20360.8</dotnetefPackageVersion> <dotnetefPackageVersion>5.0.0-preview.8.20365.2</dotnetefPackageVersion>
<MicrosoftEntityFrameworkCoreInMemoryPackageVersion>5.0.0-preview.8.20360.8</MicrosoftEntityFrameworkCoreInMemoryPackageVersion> <MicrosoftEntityFrameworkCoreInMemoryPackageVersion>5.0.0-preview.8.20365.2</MicrosoftEntityFrameworkCoreInMemoryPackageVersion>
<MicrosoftEntityFrameworkCoreRelationalPackageVersion>5.0.0-preview.8.20360.8</MicrosoftEntityFrameworkCoreRelationalPackageVersion> <MicrosoftEntityFrameworkCoreRelationalPackageVersion>5.0.0-preview.8.20365.2</MicrosoftEntityFrameworkCoreRelationalPackageVersion>
<MicrosoftEntityFrameworkCoreSqlitePackageVersion>5.0.0-preview.8.20360.8</MicrosoftEntityFrameworkCoreSqlitePackageVersion> <MicrosoftEntityFrameworkCoreSqlitePackageVersion>5.0.0-preview.8.20365.2</MicrosoftEntityFrameworkCoreSqlitePackageVersion>
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>5.0.0-preview.8.20360.8</MicrosoftEntityFrameworkCoreSqlServerPackageVersion> <MicrosoftEntityFrameworkCoreSqlServerPackageVersion>5.0.0-preview.8.20365.2</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
<MicrosoftEntityFrameworkCoreToolsPackageVersion>5.0.0-preview.8.20360.8</MicrosoftEntityFrameworkCoreToolsPackageVersion> <MicrosoftEntityFrameworkCoreToolsPackageVersion>5.0.0-preview.8.20365.2</MicrosoftEntityFrameworkCoreToolsPackageVersion>
<MicrosoftEntityFrameworkCorePackageVersion>5.0.0-preview.8.20360.8</MicrosoftEntityFrameworkCorePackageVersion> <MicrosoftEntityFrameworkCorePackageVersion>5.0.0-preview.8.20365.2</MicrosoftEntityFrameworkCorePackageVersion>
</PropertyGroup> </PropertyGroup>
<!-- <!--

View File

@ -34,7 +34,7 @@ namespace A.Internal.Namespace
[Theory] [Theory]
[MemberData(nameof(PublicMemberDefinitions))] [MemberData(nameof(PublicMemberDefinitions))]
[QuarantinedTest] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/22440")]
public async Task PublicExposureOfPubternalTypeProducesPUB0001(string member) public async Task PublicExposureOfPubternalTypeProducesPUB0001(string member)
{ {
var code = GetSourceFromNamespaceDeclaration($@" var code = GetSourceFromNamespaceDeclaration($@"

View File

@ -54,6 +54,12 @@ namespace Microsoft.AspNetCore.Components.Authorization
[Parameter] [Parameter]
public RenderFragment Authorizing { get; set; } public RenderFragment Authorizing { get; set; }
/// <summary>
/// The resource to which access is being controlled.
/// </summary>
[Parameter]
public object Resource { get; set; }
[CascadingParameter] [CascadingParameter]
private Task<AuthenticationState> ExistingCascadedAuthenticationState { get; set; } private Task<AuthenticationState> ExistingCascadedAuthenticationState { get; set; }
@ -82,6 +88,7 @@ namespace Microsoft.AspNetCore.Components.Authorization
builder.AddAttribute(2, nameof(AuthorizeRouteViewCore.Authorized), _renderAuthorizedDelegate); builder.AddAttribute(2, nameof(AuthorizeRouteViewCore.Authorized), _renderAuthorizedDelegate);
builder.AddAttribute(3, nameof(AuthorizeRouteViewCore.Authorizing), _renderAuthorizingDelegate); builder.AddAttribute(3, nameof(AuthorizeRouteViewCore.Authorizing), _renderAuthorizingDelegate);
builder.AddAttribute(4, nameof(AuthorizeRouteViewCore.NotAuthorized), _renderNotAuthorizedDelegate); builder.AddAttribute(4, nameof(AuthorizeRouteViewCore.NotAuthorized), _renderNotAuthorizedDelegate);
builder.AddAttribute(5, nameof(AuthorizeRouteViewCore.Resource), Resource);
builder.CloseComponent(); builder.CloseComponent();
} }

View File

@ -76,6 +76,78 @@ namespace Microsoft.AspNetCore.Components.Authorization
edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!")); edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!"));
} }
[Fact]
public void AuthorizesWhenResourceIsSet()
{
// Arrange
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), new Dictionary<string, object>
{
{ nameof(TestPageRequiringAuthorization.Message), "Hello, world!" }
});
var resource = "foo";
_testAuthorizationService.NextResult = AuthorizationResult.Success();
// Act
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
{ nameof(AuthorizeRouteView.Resource), resource }
}));
// Assert: renders layout
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Component<TestPageRequiringAuthorization>(batch.ReferenceFrames[edit.ReferenceFrameIndex]);
},
edit => AssertPrependText(batch, edit, "Layout ends here"));
// Assert: renders page
var pageDiff = batch.GetComponentDiffs<TestPageRequiringAuthorization>().Single();
Assert.Collection(pageDiff.Edits,
edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!"));
// Assert: Asserts that the Resource is present and set to "foo"
Assert.Collection(_testAuthorizationService.AuthorizeCalls, call=>
{
Assert.Equal(resource, call.resource.ToString());
});
}
[Fact]
public void NotAuthorizedWhenResourceMissing()
{
// Arrange
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
_testAuthorizationService.NextResult = AuthorizationResult.Failed();
// Act
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
}));
// Assert: renders layout containing "not authorized" message
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit => AssertPrependText(batch, edit, "Not authorized"),
edit => AssertPrependText(batch, edit, "Layout ends here"));
// Assert: Asserts that the Resource is Null
Assert.Collection(_testAuthorizationService.AuthorizeCalls, call=>
{
Assert.Null(call.resource);
});
}
[Fact] [Fact]
public void WhenNotAuthorized_RendersDefaultNotAuthorizedContentInsideLayout() public void WhenNotAuthorized_RendersDefaultNotAuthorizedContentInsideLayout()
{ {

View File

@ -61,6 +61,13 @@ namespace Microsoft.AspNetCore.Components
return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg); return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg);
} }
/// <summary>
/// Invokes the delegate associated with this binding and dispatches an event notification to the
/// appropriate component.
/// </summary>
/// <returns>A <see cref="Task"/> which completes asynchronously once event processing has completed.</returns>
public Task InvokeAsync() => InvokeAsync(null!);
object? IEventCallback.UnpackForRenderTree() object? IEventCallback.UnpackForRenderTree()
{ {
return RequiresExplicitReceiver ? (object)this : Delegate; return RequiresExplicitReceiver ? (object)this : Delegate;

View File

@ -56,6 +56,13 @@ namespace Microsoft.AspNetCore.Components
return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg); return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg);
} }
/// <summary>
/// Invokes the delegate associated with this binding and dispatches an event notification to the
/// appropriate component.
/// </summary>
/// <returns>A <see cref="Task"/> which completes asynchronously once event processing has completed.</returns>
public Task InvokeAsync() => InvokeAsync(default!);
internal EventCallback AsUntyped() internal EventCallback AsUntyped()
{ {
return new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate); return new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate);

View File

@ -498,6 +498,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
{ {
ProcessRenderQueue(); ProcessRenderQueue();
} }
ComponentsProfiling.Instance.End(); ComponentsProfiling.Instance.End();
} }
@ -634,11 +635,43 @@ namespace Microsoft.AspNetCore.Components.RenderTree
var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue(); var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue();
var disposeComponentState = GetRequiredComponentState(disposeComponentId); var disposeComponentState = GetRequiredComponentState(disposeComponentId);
Log.DisposingComponent(_logger, disposeComponentState); Log.DisposingComponent(_logger, disposeComponentState);
if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var exception)) if (!(disposeComponentState.Component is IAsyncDisposable))
{ {
exceptions ??= new List<Exception>(); if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var exception))
exceptions.Add(exception); {
exceptions ??= new List<Exception>();
exceptions.Add(exception);
}
} }
else
{
var result = disposeComponentState.DisposeInBatchAsync(_batchBuilder);
if (result.IsCompleted)
{
if (!result.IsCompletedSuccessfully)
{
exceptions ??= new List<Exception>();
exceptions.Add(result.Exception);
}
}
else
{
AddToPendingTasks(GetHandledAsynchronousDisposalErrorsTask(result));
async Task GetHandledAsynchronousDisposalErrorsTask(Task result)
{
try
{
await result;
}
catch (Exception e)
{
HandleException(e);
}
}
}
}
_componentStateById.Remove(disposeComponentId); _componentStateById.Remove(disposeComponentId);
_batchBuilder.DisposedComponentIds.Append(disposeComponentId); _batchBuilder.DisposedComponentIds.Append(disposeComponentId);
} }

View File

@ -101,6 +101,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
exception = ex; exception = ex;
} }
CleanupComponentStateResources(batchBuilder);
return exception == null;
}
private void CleanupComponentStateResources(RenderBatchBuilder batchBuilder)
{
// We don't expect these things to throw. // We don't expect these things to throw.
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames()); RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames());
@ -110,8 +117,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
} }
DisposeBuffers(); DisposeBuffers();
return exception == null;
} }
// Callers expect this method to always return a faulted task. // Callers expect this method to always return a faulted task.
@ -222,5 +227,31 @@ namespace Microsoft.AspNetCore.Components.Rendering
((IDisposable)CurrentRenderTree).Dispose(); ((IDisposable)CurrentRenderTree).Dispose();
_latestDirectParametersSnapshot?.Dispose(); _latestDirectParametersSnapshot?.Dispose();
} }
public Task DisposeInBatchAsync(RenderBatchBuilder batchBuilder)
{
_componentWasDisposed = true;
CleanupComponentStateResources(batchBuilder);
try
{
var result = ((IAsyncDisposable)Component).DisposeAsync();
if (result.IsCompletedSuccessfully)
{
return Task.CompletedTask;
}
else
{
// We know we are dealing with an exception that happened asynchronously, so return a task
// to the caller so that he can unwrap it.
return result.AsTask();
}
}
catch (Exception e)
{
return Task.FromException(e);
}
}
} }
} }

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Components.Routing
public override bool Match(string pathSegment, out object? convertedValue) public override bool Match(string pathSegment, out object? convertedValue)
{ {
// Unset values are set to null in the Parameters object created in // Unset values are set to null in the Parameters object created in
// the RouteContext. To match this pattern, unset optional parmeters // the RouteContext. To match this pattern, unset optional parameters
// are converted to null. // are converted to null.
if (string.IsNullOrEmpty(pathSegment)) if (string.IsNullOrEmpty(pathSegment))
{ {

View File

@ -29,10 +29,20 @@ namespace Microsoft.AspNetCore.Components.Routing
internal void Match(RouteContext context) internal void Match(RouteContext context)
{ {
string? catchAllValue = null;
// If this template contains a catch-all parameter, we can concatenate the pathSegments
// at and beyond the catch-all segment's position. For example:
// Template: /foo/bar/{*catchAll}
// PathSegments: /foo/bar/one/two/three
if (Template.ContainsCatchAllSegment && context.Segments.Length >= Template.Segments.Length)
{
catchAllValue = string.Join('/', context.Segments[Range.StartAt(Template.Segments.Length - 1)]);
}
// If there are no optional segments on the route and the length of the route // If there are no optional segments on the route and the length of the route
// and the template do not match, then there is no chance of this matching and // and the template do not match, then there is no chance of this matching and
// we can bail early. // we can bail early.
if (Template.OptionalSegmentsCount == 0 && Template.Segments.Length != context.Segments.Length) else if (Template.OptionalSegmentsCount == 0 && Template.Segments.Length != context.Segments.Length)
{ {
return; return;
} }
@ -43,7 +53,15 @@ namespace Microsoft.AspNetCore.Components.Routing
for (var i = 0; i < Template.Segments.Length; i++) for (var i = 0; i < Template.Segments.Length; i++)
{ {
var segment = Template.Segments[i]; var segment = Template.Segments[i];
if (segment.IsCatchAll)
{
numMatchingSegments += 1;
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
parameters[segment.Value] = catchAllValue;
break;
}
// If the template contains more segments than the path, then // If the template contains more segments than the path, then
// we may need to break out of this for-loop. This can happen // we may need to break out of this for-loop. This can happen
// in one of two cases: // in one of two cases:
@ -86,7 +104,7 @@ namespace Microsoft.AspNetCore.Components.Routing
// In addition to extracting parameter values from the URL, each route entry // In addition to extracting parameter values from the URL, each route entry
// also knows which other parameters should be supplied with null values. These // also knows which other parameters should be supplied with null values. These
// are parameters supplied by other route entries matching the same handler. // are parameters supplied by other route entries matching the same handler.
if (UnusedRouteParameterNames.Length > 0) if (!Template.ContainsCatchAllSegment && UnusedRouteParameterNames.Length > 0)
{ {
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal); parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
for (var i = 0; i < UnusedRouteParameterNames.Length; i++) for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
@ -116,7 +134,7 @@ namespace Microsoft.AspNetCore.Components.Routing
// `/this/is/a/template` and the route `/this/`. In that case, we want to ensure // `/this/is/a/template` and the route `/this/`. In that case, we want to ensure
// that all non-optional segments have matched as well. // that all non-optional segments have matched as well.
var allNonOptionalSegmentsMatch = numMatchingSegments >= (Template.Segments.Length - Template.OptionalSegmentsCount); var allNonOptionalSegmentsMatch = numMatchingSegments >= (Template.Segments.Length - Template.OptionalSegmentsCount);
if (allRouteSegmentsMatch && allNonOptionalSegmentsMatch) if (Template.ContainsCatchAllSegment || (allRouteSegmentsMatch && allNonOptionalSegmentsMatch))
{ {
context.Parameters = parameters; context.Parameters = parameters;
context.Handler = Handler; context.Handler = Handler;

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Components.Routing
TemplateText = templateText; TemplateText = templateText;
Segments = segments; Segments = segments;
OptionalSegmentsCount = segments.Count(template => template.IsOptional); OptionalSegmentsCount = segments.Count(template => template.IsOptional);
ContainsCatchAllSegment = segments.Any(template => template.IsCatchAll);
} }
public string TemplateText { get; } public string TemplateText { get; }
@ -22,5 +23,7 @@ namespace Microsoft.AspNetCore.Components.Routing
public TemplateSegment[] Segments { get; } public TemplateSegment[] Segments { get; }
public int OptionalSegmentsCount { get; } public int OptionalSegmentsCount { get; }
public bool ContainsCatchAllSegment { get; }
} }
} }

View File

@ -12,15 +12,15 @@ namespace Microsoft.AspNetCore.Components.Routing
// The class in here just takes care of parsing a route and extracting // The class in here just takes care of parsing a route and extracting
// simple parameters from it. // simple parameters from it.
// Some differences with ASP.NET Core routes are: // Some differences with ASP.NET Core routes are:
// * We don't support catch all parameter segments.
// * We don't support complex segments. // * We don't support complex segments.
// The things that we support are: // The things that we support are:
// * Literal path segments. (Like /Path/To/Some/Page) // * Literal path segments. (Like /Path/To/Some/Page)
// * Parameter path segments (Like /Customer/{Id}/Orders/{OrderId}) // * Parameter path segments (Like /Customer/{Id}/Orders/{OrderId})
// * Catch-all parameters (Like /blog/{*slug})
internal class TemplateParser internal class TemplateParser
{ {
public static readonly char[] InvalidParameterNameCharacters = public static readonly char[] InvalidParameterNameCharacters =
new char[] { '*', '{', '}', '=', '.' }; new char[] { '{', '}', '=', '.' };
internal static RouteTemplate ParseTemplate(string template) internal static RouteTemplate ParseTemplate(string template)
{ {
@ -80,6 +80,12 @@ namespace Microsoft.AspNetCore.Components.Routing
for (int i = 0; i < templateSegments.Length; i++) for (int i = 0; i < templateSegments.Length; i++)
{ {
var currentSegment = templateSegments[i]; var currentSegment = templateSegments[i];
if (currentSegment.IsCatchAll && i != templateSegments.Length - 1)
{
throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter can only appear as the last segment of the route template.");
}
if (!currentSegment.IsParameter) if (!currentSegment.IsParameter)
{ {
continue; continue;

View File

@ -12,34 +12,48 @@ namespace Microsoft.AspNetCore.Components.Routing
{ {
IsParameter = isParameter; IsParameter = isParameter;
IsCatchAll = segment.StartsWith('*');
if (IsCatchAll)
{
// Only one '*' currently allowed
Value = segment.Substring(1);
var invalidCharacter = Value.IndexOf('*');
if (Value.IndexOf('*') != -1)
{
throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter may only have one '*' at the beginning of the segment.");
}
}
else
{
Value = segment;
}
// Process segments that are not parameters or do not contain // Process segments that are not parameters or do not contain
// a token separating a type constraint. // a token separating a type constraint.
if (!isParameter || segment.IndexOf(':') < 0) if (!isParameter || Value.IndexOf(':') < 0)
{ {
// Set the IsOptional flag to true for segments that contain // Set the IsOptional flag to true for segments that contain
// a parameter with no type constraints but optionality set // a parameter with no type constraints but optionality set
// via the '?' token. // via the '?' token.
if (segment.IndexOf('?') == segment.Length - 1) if (Value.IndexOf('?') == Value.Length - 1)
{ {
IsOptional = true; IsOptional = true;
Value = segment.Substring(0, segment.Length - 1); Value = Value.Substring(0, Value.Length - 1);
} }
// If the `?` optional marker shows up in the segment but not at the very end, // If the `?` optional marker shows up in the segment but not at the very end,
// then throw an error. // then throw an error.
else if (segment.IndexOf('?') >= 0 && segment.IndexOf('?') != segment.Length - 1) else if (Value.IndexOf('?') >= 0 && Value.IndexOf('?') != Value.Length - 1)
{ {
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name."); throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name.");
} }
else
{
Value = segment;
}
Constraints = Array.Empty<RouteConstraint>(); Constraints = Array.Empty<RouteConstraint>();
} }
else else
{ {
var tokens = segment.Split(':'); var tokens = Value.Split(':');
if (tokens[0].Length == 0) if (tokens[0].Length == 0)
{ {
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list."); throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list.");
@ -54,6 +68,21 @@ namespace Microsoft.AspNetCore.Components.Routing
.Select(token => RouteConstraint.Parse(template, segment, token)) .Select(token => RouteConstraint.Parse(template, segment, token))
.ToArray(); .ToArray();
} }
if (IsParameter)
{
if (IsOptional && IsCatchAll)
{
throw new InvalidOperationException($"Invalid segment '{segment}' in route '{template}'. A catch-all parameter cannot be marked optional.");
}
// Moving the check for this here instead of TemplateParser so we can allow catch-all.
// We checked for '*' up above specifically for catch-all segments, this one checks for all others
if (Value.IndexOf('*') != -1)
{
throw new InvalidOperationException($"Invalid template '{template}'. The character '*' in parameter segment '{{{segment}}}' is not allowed.");
}
}
} }
// The value of the segment. The exact text to match when is a literal. // The value of the segment. The exact text to match when is a literal.
@ -64,6 +93,8 @@ namespace Microsoft.AspNetCore.Components.Routing
public bool IsOptional { get; } public bool IsOptional { get; }
public bool IsCatchAll { get; }
public RouteConstraint[] Constraints { get; } public RouteConstraint[] Constraints { get; }
public bool Match(string pathSegment, out object? matchedParameterValue) public bool Match(string pathSegment, out object? matchedParameterValue)

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Components
var callback = default(EventCallback); var callback = default(EventCallback);
// Act & Assert (Does not throw) // Act & Assert (Does not throw)
await callback.InvokeAsync(null); await callback.InvokeAsync();
} }
[Fact] [Fact]
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Components
var callback = default(EventCallback<EventArgs>); var callback = default(EventCallback<EventArgs>);
// Act & Assert (Does not throw) // Act & Assert (Does not throw)
await callback.InvokeAsync(null); await callback.InvokeAsync();
} }
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback(null, (Action)(() => runCount++)); var callback = new EventCallback(null, (Action)(() => runCount++));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback<EventArgs>(null, (Action)(() => runCount++)); var callback = new EventCallback<EventArgs>(null, (Action)(() => runCount++));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback(component, (Action)(() => runCount++)); var callback = new EventCallback(component, (Action)(() => runCount++));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback(component, (Action<EventArgs>)((e) => { arg = e; runCount++; })); var callback = new EventCallback(component, (Action<EventArgs>)((e) => { arg = e; runCount++; }));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback(component, (Func<Task>)(() => { runCount++; return Task.CompletedTask; })); var callback = new EventCallback(component, (Func<Task>)(() => { runCount++; return Task.CompletedTask; }));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback(component, (Func<EventArgs, Task>)((e) => { arg = e; runCount++; return Task.CompletedTask; })); var callback = new EventCallback(component, (Func<EventArgs, Task>)((e) => { arg = e; runCount++; return Task.CompletedTask; }));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback<EventArgs>(component, (Action)(() => runCount++)); var callback = new EventCallback<EventArgs>(component, (Action)(() => runCount++));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback<EventArgs>(component, (Action<EventArgs>)((e) => { arg = e; runCount++; })); var callback = new EventCallback<EventArgs>(component, (Action<EventArgs>)((e) => { arg = e; runCount++; }));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -373,7 +373,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback<EventArgs>(component, (Func<Task>)(() => { runCount++; return Task.CompletedTask; })); var callback = new EventCallback<EventArgs>(component, (Func<Task>)(() => { runCount++; return Task.CompletedTask; }));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert
@ -410,7 +410,7 @@ namespace Microsoft.AspNetCore.Components
var callback = new EventCallback<EventArgs>(component, (Func<EventArgs, Task>)((e) => { arg = e; runCount++; return Task.CompletedTask; })); var callback = new EventCallback<EventArgs>(component, (Func<EventArgs, Task>)((e) => { arg = e; runCount++; return Task.CompletedTask; }));
// Act // Act
await callback.InvokeAsync(null); await callback.InvokeAsync();
// Assert // Assert

View File

@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Components
} }
[Fact] [Fact]
public void IncomingParameterMatchesOverridenParameter_ThatDoesNotHasAttribute() public void IncomingParameterMatchesOverriddenParameter_ThatDoesNotHaveAttribute()
{ {
// Test for https://github.com/dotnet/aspnetcore/issues/13162 // Test for https://github.com/dotnet/aspnetcore/issues/13162
// Arrange // Arrange

View File

@ -487,7 +487,8 @@ namespace Microsoft.AspNetCore.Components.Test
public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers() public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers()
{ {
// Arrange: Render a component with an event handler // Arrange: Render a component with an event handler
var renderer = new TestRenderer { var renderer = new TestRenderer
{
ShouldHandleExceptions = true ShouldHandleExceptions = true
}; };
@ -2086,6 +2087,238 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Contains(exception2, aex.InnerExceptions); Assert.Contains(exception2, aex.InnerExceptions);
} }
[Fact]
public void RenderBatch_HandlesSynchronousExceptionsInAsyncDisposableComponents()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new InvalidOperationException();
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => throw exception1));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
var innerException = Assert.Single(aex.Flatten().InnerExceptions);
Assert.Same(exception1, innerException);
}
[Fact]
public void RenderBatch_CanDisposeSynchronousAsyncDisposableImplementations()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => default));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void RenderBatch_CanDisposeAsynchronousAsyncDisposables()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer { ShouldHandleExceptions = true };
renderer.OnExceptionHandled = () => semaphore.Release();
var exception1 = new InvalidOperationException();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => { await tcs.Task; }));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Continue execution
tcs.SetResult();
Assert.False(semaphore.WaitOne(10));
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void RenderBatch_HandlesAsynchronousExceptionsInAsyncDisposableComponents()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer { ShouldHandleExceptions = true };
renderer.OnExceptionHandled = () => semaphore.Release();
var exception1 = new InvalidOperationException();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => { await tcs.Task; throw exception1; }));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Continue execution
tcs.SetResult();
semaphore.WaitOne();
var aex = Assert.IsType<InvalidOperationException>(Assert.Single(renderer.HandledExceptions));
Assert.Same(exception1, aex);
}
[Fact]
public void RenderBatch_ReportsSynchronousCancelationsAsErrors()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => throw new TaskCanceledException()));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
Assert.IsType<TaskCanceledException>(Assert.Single(aex.Flatten().InnerExceptions));
}
[Fact]
public void RenderBatch_ReportsAsynchronousCancelationsAsErrors()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer { ShouldHandleExceptions = true };
renderer.OnExceptionHandled += () => semaphore.Release();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(
1,
nameof(AsyncDisposableComponent.AsyncDisposeAction),
(Func<ValueTask>)(() => new ValueTask(tcs.Task)));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Cancel execution
tcs.SetCanceled();
semaphore.WaitOne();
var aex = Assert.IsType<TaskCanceledException>(Assert.Single(renderer.HandledExceptions));
}
[Fact] [Fact]
public void RenderBatch_DoesNotDisposeComponentMultipleTimes() public void RenderBatch_DoesNotDisposeComponentMultipleTimes()
{ {
@ -2589,7 +2822,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act: Toggle the checkbox // Act: Toggle the checkbox
var eventArgs = new ChangeEventArgs { Value = true }; var eventArgs = new ChangeEventArgs { Value = true };
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs); var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully); Assert.True(renderTask.IsCompletedSuccessfully);
var latestBatch = renderer.Batches.Last(); var latestBatch = renderer.Batches.Last();
@ -3768,7 +4001,7 @@ namespace Microsoft.AspNetCore.Components.Test
requestedType => Assert.Equal(typeof(TestComponent), requestedType)); requestedType => Assert.Equal(typeof(TestComponent), requestedType));
} }
private class TestComponentActivator<TResult> : IComponentActivator where TResult: IComponent, new() private class TestComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
{ {
public List<Type> RequestedComponentTypes { get; } = new List<Type>(); public List<Type> RequestedComponentTypes { get; } = new List<Type>();
@ -4147,6 +4380,24 @@ namespace Microsoft.AspNetCore.Components.Test
} }
} }
private class AsyncDisposableComponent : AutoRenderComponent, IAsyncDisposable
{
public bool Disposed { get; private set; }
[Parameter]
public Func<ValueTask> AsyncDisposeAction { get; set; }
public ValueTask DisposeAsync()
{
Disposed = true;
return AsyncDisposeAction == null ? default : AsyncDisposeAction.Invoke();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
}
}
class TestAsyncRenderer : TestRenderer class TestAsyncRenderer : TestRenderer
{ {
public Task NextUpdateDisplayReturnTask { get; set; } public Task NextUpdateDisplayReturnTask { get; set; }

View File

@ -226,6 +226,23 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue); Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue);
} }
[Theory]
[InlineData("/blog/value1", "value1")]
[InlineData("/blog/value1/foo%20bar", "value1/foo bar")]
public void CanMatchCatchAllParameterTemplate(string path, string expectedValue)
{
// Arrange
var routeTable = new TestRouteTableBuilder().AddRoute("/blog/{*parameter}").Build();
var context = new RouteContext(path);
// Act
routeTable.Route(context);
// Assert
Assert.NotNull(context.Handler);
Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue);
}
[Fact] [Fact]
public void CanMatchTemplateWithMultipleParameters() public void CanMatchTemplateWithMultipleParameters()
{ {
@ -247,6 +264,29 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
Assert.Equal(expectedParameters, context.Parameters); Assert.Equal(expectedParameters, context.Parameters);
} }
[Fact]
public void CanMatchTemplateWithMultipleParametersAndCatchAllParameter()
{
// Arrange
var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/with/{*catchAll}").Build();
var context = new RouteContext("/an/awesome/path/with/some/catch/all/stuff");
var expectedParameters = new Dictionary<string, object>
{
["some"] = "an",
["route"] = "path",
["catchAll"] = "some/catch/all/stuff"
};
// Act
routeTable.Route(context);
// Assert
Assert.NotNull(context.Handler);
Assert.Equal(expectedParameters, context.Parameters);
}
public static IEnumerable<object[]> CanMatchParameterWithConstraintCases() => new object[][] public static IEnumerable<object[]> CanMatchParameterWithConstraintCases() => new object[][]
{ {
new object[] { "/{value:bool}", "/true", true }, new object[] { "/{value:bool}", "/true", true },
@ -476,7 +516,7 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
[Fact] [Fact]
public void PrefersLiteralTemplateOverParmeterizedTemplates() public void PrefersLiteralTemplateOverParameterizedTemplates()
{ {
// Arrange // Arrange
var routeTable = new TestRouteTableBuilder() var routeTable = new TestRouteTableBuilder()

View File

@ -83,6 +83,45 @@ namespace Microsoft.AspNetCore.Components.Routing
Assert.Equal(expected, actual, RouteTemplateTestComparer.Instance); Assert.Equal(expected, actual, RouteTemplateTestComparer.Instance);
} }
[Fact]
public void Parse_SingleCatchAllParameter()
{
// Arrange
var expected = new ExpectedTemplateBuilder().Parameter("p");
// Act
var actual = TemplateParser.ParseTemplate("{*p}");
// Assert
Assert.Equal(expected, actual, RouteTemplateTestComparer.Instance);
}
[Fact]
public void Parse_MixedLiteralAndCatchAllParameter()
{
// Arrange
var expected = new ExpectedTemplateBuilder().Literal("awesome").Literal("wow").Parameter("p");
// Act
var actual = TemplateParser.ParseTemplate("awesome/wow/{*p}");
// Assert
Assert.Equal(expected, actual, RouteTemplateTestComparer.Instance);
}
[Fact]
public void Parse_MixedLiteralParameterAndCatchAllParameter()
{
// Arrange
var expected = new ExpectedTemplateBuilder().Literal("awesome").Parameter("p1").Parameter("p2");
// Act
var actual = TemplateParser.ParseTemplate("awesome/{p1}/{*p2}");
// Assert
Assert.Equal(expected, actual, RouteTemplateTestComparer.Instance);
}
[Fact] [Fact]
public void InvalidTemplate_WithRepeatedParameter() public void InvalidTemplate_WithRepeatedParameter()
{ {
@ -113,7 +152,8 @@ namespace Microsoft.AspNetCore.Components.Routing
} }
[Theory] [Theory]
[InlineData("{*}", "Invalid template '{*}'. The character '*' in parameter segment '{*}' is not allowed.")] // * is only allowed at beginning for catch-all parameters
[InlineData("{p*}", "Invalid template '{p*}'. The character '*' in parameter segment '{p*}' is not allowed.")]
[InlineData("{{}", "Invalid template '{{}'. The character '{' in parameter segment '{{}' is not allowed.")] [InlineData("{{}", "Invalid template '{{}'. The character '{' in parameter segment '{{}' is not allowed.")]
[InlineData("{}}", "Invalid template '{}}'. The character '}' in parameter segment '{}}' is not allowed.")] [InlineData("{}}", "Invalid template '{}}'. The character '}' in parameter segment '{}}' is not allowed.")]
[InlineData("{=}", "Invalid template '{=}'. The character '=' in parameter segment '{=}' is not allowed.")] [InlineData("{=}", "Invalid template '{=}'. The character '=' in parameter segment '{=}' is not allowed.")]
@ -166,6 +206,26 @@ namespace Microsoft.AspNetCore.Components.Routing
Assert.Equal(expectedMessage, ex.Message); Assert.Equal(expectedMessage, ex.Message);
} }
[Fact]
public void InvalidTemplate_CatchAllParamWithMultipleAsterisks()
{
var ex = Assert.Throws<InvalidOperationException>(() => TemplateParser.ParseTemplate("/test/{a}/{**b}"));
var expectedMessage = "Invalid template '/test/{a}/{**b}'. A catch-all parameter may only have one '*' at the beginning of the segment.";
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public void InvalidTemplate_CatchAllParamNotLast()
{
var ex = Assert.Throws<InvalidOperationException>(() => TemplateParser.ParseTemplate("/test/{*a}/{b}"));
var expectedMessage = "Invalid template 'test/{*a}/{b}'. A catch-all parameter can only appear as the last segment of the route template.";
Assert.Equal(expectedMessage, ex.Message);
}
[Fact] [Fact]
public void InvalidTemplate_BadOptionalCharacterPosition() public void InvalidTemplate_BadOptionalCharacterPosition()
{ {

View File

@ -24,6 +24,7 @@ app {
height: 3.5rem; height: 3.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
z-index: 10;
} }
.main { .main {

View File

@ -296,7 +296,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
exceptions.Add(e); exceptions.Add(e);
}; };
// Receive the ack for the intial batch // Receive the ack for the initial batch
_ = renderer.OnRenderCompletedAsync(2, null); _ = renderer.OnRenderCompletedAsync(2, null);
// Receive the ack for the second batch // Receive the ack for the second batch
_ = renderer.OnRenderCompletedAsync(2, null); _ = renderer.OnRenderCompletedAsync(2, null);

View File

@ -71,6 +71,7 @@ namespace Microsoft.AspNetCore.Components.Web
"touch" => Deserialize<TouchEventArgs>(eventArgsJson), "touch" => Deserialize<TouchEventArgs>(eventArgsJson),
"unknown" => EventArgs.Empty, "unknown" => EventArgs.Empty,
"wheel" => Deserialize<WheelEventArgs>(eventArgsJson), "wheel" => Deserialize<WheelEventArgs>(eventArgsJson),
"toggle" => Deserialize<EventArgs>(eventArgsJson),
_ => throw new InvalidOperationException($"Unsupported event type '{eventArgsType}'. EventId: '{eventHandlerId}'."), _ => throw new InvalidOperationException($"Unsupported event type '{eventArgsType}'. EventId: '{eventHandlerId}'."),
}; };
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -66,8 +66,11 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
); );
}); });
// Get the custom environment setting if defined
const environment = options?.environment;
// Fetch the resources and prepare the Mono runtime // Fetch the resources and prepare the Mono runtime
const bootConfigResult = await BootConfigResult.initAsync(); const bootConfigResult = await BootConfigResult.initAsync(environment);
const [resourceLoader] = await Promise.all([ const [resourceLoader] = await Promise.all([
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}), WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),

View File

@ -2,7 +2,7 @@ export class BootConfigResult {
private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) { private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) {
} }
static async initAsync(): Promise<BootConfigResult> { static async initAsync(environment?: string): Promise<BootConfigResult> {
const bootConfigResponse = await fetch('_framework/blazor.boot.json', { const bootConfigResponse = await fetch('_framework/blazor.boot.json', {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
@ -10,8 +10,8 @@ export class BootConfigResult {
}); });
// While we can expect an ASP.NET Core hosted application to include the environment, other // While we can expect an ASP.NET Core hosted application to include the environment, other
// hosts may not. Assume 'Production' in the absenc of any specified value. // hosts may not. Assume 'Production' in the absence of any specified value.
const applicationEnvironment = bootConfigResponse.headers.get('Blazor-Environment') || 'Production'; const applicationEnvironment = environment || bootConfigResponse.headers.get('Blazor-Environment') || 'Production';
const bootConfig: BootJsonData = await bootConfigResponse.json(); const bootConfig: BootJsonData = await bootConfigResponse.json();
return new BootConfigResult(bootConfig, applicationEnvironment); return new BootConfigResult(bootConfig, applicationEnvironment);

View File

@ -1,3 +1,6 @@
// Import type definitions to ensure that the global declaration
// is of BINDING is included when tests run
import './Mono/MonoTypes';
import { System_String } from './Platform'; import { System_String } from './Platform';
interface TimingEntry { interface TimingEntry {

View File

@ -165,6 +165,12 @@ async function getCacheToUseIfEnabled(bootConfig: BootJsonData): Promise<Cache |
return null; return null;
} }
// cache integrity is compromised if the first request has been served over http
// in this case, we want to disable caching and integrity validation
if (document.location.protocol !== 'https:') {
return null;
}
// Define a separate cache for each base href, so we're isolated from any other // Define a separate cache for each base href, so we're isolated from any other
// Blazor application running on the same origin. We need this so that we're free // Blazor application running on the same origin. We need this so that we're free
// to purge from the cache anything we're not using and don't let it keep growing, // to purge from the cache anything we're not using and don't let it keep growing,

View File

@ -9,6 +9,11 @@ export interface WebAssemblyStartOptions {
* @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior. * @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior.
*/ */
loadBootResource(type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) : string | Promise<Response> | null | undefined; loadBootResource(type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) : string | Promise<Response> | null | undefined;
/**
* Override built-in environment setting on start.
*/
environment?: string;
} }
// This type doesn't have to align with anything in BootConfig. // This type doesn't have to align with anything in BootConfig.

View File

@ -238,7 +238,8 @@ export class BrowserRenderer {
document.createElementNS('http://www.w3.org/2000/svg', tagName) : document.createElementNS('http://www.w3.org/2000/svg', tagName) :
document.createElement(tagName); document.createElement(tagName);
const newElement = toLogicalElement(newDomElementRaw); const newElement = toLogicalElement(newDomElementRaw);
insertLogicalChild(newDomElementRaw, parent, childIndex);
let inserted = false;
// Apply attributes // Apply attributes
const descendantsEndIndexExcl = frameIndex + frameReader.subtreeLength(frame); const descendantsEndIndexExcl = frameIndex + frameReader.subtreeLength(frame);
@ -247,6 +248,8 @@ export class BrowserRenderer {
if (frameReader.frameType(descendantFrame) === FrameType.attribute) { if (frameReader.frameType(descendantFrame) === FrameType.attribute) {
this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame); this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame);
} else { } else {
insertLogicalChild(newDomElementRaw, parent, childIndex);
inserted = true;
// As soon as we see a non-attribute child, all the subsequent child frames are // As soon as we see a non-attribute child, all the subsequent child frames are
// not attributes, so bail out and insert the remnants recursively // not attributes, so bail out and insert the remnants recursively
this.insertFrameRange(batch, componentId, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl); this.insertFrameRange(batch, componentId, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl);
@ -254,17 +257,40 @@ export class BrowserRenderer {
} }
} }
// We handle setting 'value' on a <select> in two different ways: // this element did not have any children, so it's not inserted yet.
// [1] When inserting a corresponding <option>, in case you're dynamically adding options if (!inserted) {
insertLogicalChild(newDomElementRaw, parent, childIndex);
}
// We handle setting 'value' on a <select> in three different ways:
// [1] When inserting a corresponding <option>, in case you're dynamically adding options.
// This is the case below.
// [2] After we finish inserting the <select>, in case the descendant options are being // [2] After we finish inserting the <select>, in case the descendant options are being
// added as an opaque markup block rather than individually // added as an opaque markup block rather than individually. This is the other case below.
// Right here we implement [2] // [3] In case the the value of the select and the option value is changed in the same batch.
if (newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw) { // We just receive an attribute frame and have to set the select value afterwards.
if (newDomElementRaw instanceof HTMLOptionElement)
{
// Situation 1
this.trySetSelectValueFromOptionElement(newDomElementRaw);
} else if (newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw) {
// Situation 2
const selectValue: string | null = newDomElementRaw[selectValuePropname]; const selectValue: string | null = newDomElementRaw[selectValuePropname];
setSelectElementValue(newDomElementRaw, selectValue); setSelectElementValue(newDomElementRaw, selectValue);
} }
} }
private trySetSelectValueFromOptionElement(optionElement: HTMLOptionElement) {
const selectElem = this.findClosestAncestorSelectElement(optionElement);
if (selectElem && (selectValuePropname in selectElem) && selectElem[selectValuePropname] === optionElement.value) {
setSelectElementValue(selectElem, optionElement.value);
delete selectElem[selectValuePropname];
return true;
}
return false;
}
private insertComponent(batch: RenderBatch, parent: LogicalElement, childIndex: number, frame: RenderTreeFrame) { private insertComponent(batch: RenderBatch, parent: LogicalElement, childIndex: number, frame: RenderTreeFrame) {
const containerElement = createAndInsertLogicalContainer(parent, childIndex); const containerElement = createAndInsertLogicalContainer(parent, childIndex);
@ -385,13 +411,10 @@ export class BrowserRenderer {
} else { } else {
element.removeAttribute('value'); element.removeAttribute('value');
} }
// See above for why we have this special handling for <select>/<option> // See above for why we have this special handling for <select>/<option>
// Note that this is only one of the two cases where we set the value on a <select> // Situation 3
const selectElem = this.findClosestAncestorSelectElement(element); this.trySetSelectValueFromOptionElement(<HTMLOptionElement>element);
if (selectElem && (selectValuePropname in selectElem) && selectElem[selectValuePropname] === value) {
this.tryApplyValueProperty(batch, selectElem, attributeFrame);
delete selectElem[selectValuePropname];
}
return true; return true;
} }
default: default:

View File

@ -17,6 +17,7 @@ const nonBubblingEvents = toLookup([
'scroll', 'scroll',
'submit', 'submit',
'unload', 'unload',
'toggle',
'DOMNodeInsertedIntoDocument', 'DOMNodeInsertedIntoDocument',
'DOMNodeRemovedFromDocument', 'DOMNodeRemovedFromDocument',
]); ]);

View File

@ -89,6 +89,9 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'mousewheel': case 'mousewheel':
return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(event as WheelEvent)); return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(event as WheelEvent));
case 'toggle':
return new EventForDotNet<UIEventArgs>('toggle', { type: event.type });
default: default:
return new EventForDotNet<UIEventArgs>('unknown', { type: event.type }); return new EventForDotNet<UIEventArgs>('unknown', { type: event.type });
} }
@ -248,7 +251,7 @@ function normalizeTimeBasedValue(element: HTMLInputElement): string {
// The following interfaces must be kept in sync with the UIEventArgs C# classes // The following interfaces must be kept in sync with the UIEventArgs C# classes
export type EventArgsType = 'change' | 'clipboard' | 'drag' | 'error' | 'focus' | 'keyboard' | 'mouse' | 'pointer' | 'progress' | 'touch' | 'unknown' | 'wheel'; export type EventArgsType = 'change' | 'clipboard' | 'drag' | 'error' | 'focus' | 'keyboard' | 'mouse' | 'pointer' | 'progress' | 'touch' | 'unknown' | 'wheel' | 'toggle';
export interface UIEventArgs { export interface UIEventArgs {
type: string; type: string;

View File

@ -16,7 +16,8 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
private readonly Func<Task> _handleSubmitDelegate; // Cache to avoid per-render allocations private readonly Func<Task> _handleSubmitDelegate; // Cache to avoid per-render allocations
private EditContext? _fixedEditContext; private EditContext? _editContext;
private bool _hasSetEditContextExplicitly;
/// <summary> /// <summary>
/// Constructs an instance of <see cref="EditForm"/>. /// Constructs an instance of <see cref="EditForm"/>.
@ -36,7 +37,16 @@ namespace Microsoft.AspNetCore.Components.Forms
/// also supply <see cref="Model"/>, since the model value will be taken /// also supply <see cref="Model"/>, since the model value will be taken
/// from the <see cref="EditContext.Model"/> property. /// from the <see cref="EditContext.Model"/> property.
/// </summary> /// </summary>
[Parameter] public EditContext? EditContext { get; set; } [Parameter]
public EditContext? EditContext
{
get => _editContext;
set
{
_editContext = value;
_hasSetEditContextExplicitly = value != null;
}
}
/// <summary> /// <summary>
/// Specifies the top-level model object for the form. An edit context will /// Specifies the top-level model object for the form. An edit context will
@ -73,11 +83,16 @@ namespace Microsoft.AspNetCore.Components.Forms
/// <inheritdoc /> /// <inheritdoc />
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
if ((EditContext == null) == (Model == null)) if (_hasSetEditContextExplicitly && Model != null)
{ {
throw new InvalidOperationException($"{nameof(EditForm)} requires a {nameof(Model)} " + throw new InvalidOperationException($"{nameof(EditForm)} requires a {nameof(Model)} " +
$"parameter, or an {nameof(EditContext)} parameter, but not both."); $"parameter, or an {nameof(EditContext)} parameter, but not both.");
} }
else if (!_hasSetEditContextExplicitly && Model == null)
{
throw new InvalidOperationException($"{nameof(EditForm)} requires either a {nameof(Model)} " +
$"parameter, or an {nameof(EditContext)} parameter, please provide one of these.");
}
// If you're using OnSubmit, it becomes your responsibility to trigger validation manually // If you're using OnSubmit, it becomes your responsibility to trigger validation manually
// (e.g., so you can display a "pending" state in the UI). In that case you don't want the // (e.g., so you can display a "pending" state in the UI). In that case you don't want the
@ -89,31 +104,31 @@ namespace Microsoft.AspNetCore.Components.Forms
$"{nameof(EditForm)}, do not also supply {nameof(OnValidSubmit)} or {nameof(OnInvalidSubmit)}."); $"{nameof(EditForm)}, do not also supply {nameof(OnValidSubmit)} or {nameof(OnInvalidSubmit)}.");
} }
// Update _fixedEditContext if we don't have one yet, or if they are supplying a // Update _editContext if we don't have one yet, or if they are supplying a
// potentially new EditContext, or if they are supplying a different Model // potentially new EditContext, or if they are supplying a different Model
if (_fixedEditContext == null || EditContext != null || Model != _fixedEditContext.Model) if (Model != null && Model != _editContext?.Model)
{ {
_fixedEditContext = EditContext ?? new EditContext(Model!); _editContext = new EditContext(Model!);
} }
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder) protected override void BuildRenderTree(RenderTreeBuilder builder)
{ {
Debug.Assert(_fixedEditContext != null); Debug.Assert(_editContext != null);
// If _fixedEditContext changes, tear down and recreate all descendants. // If _editContext changes, tear down and recreate all descendants.
// This is so we can safely use the IsFixed optimization on CascadingValue, // This is so we can safely use the IsFixed optimization on CascadingValue,
// optimizing for the common case where _fixedEditContext never changes. // optimizing for the common case where _editContext never changes.
builder.OpenRegion(_fixedEditContext.GetHashCode()); builder.OpenRegion(_editContext.GetHashCode());
builder.OpenElement(0, "form"); builder.OpenElement(0, "form");
builder.AddMultipleAttributes(1, AdditionalAttributes); builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "onsubmit", _handleSubmitDelegate); builder.AddAttribute(2, "onsubmit", _handleSubmitDelegate);
builder.OpenComponent<CascadingValue<EditContext>>(3); builder.OpenComponent<CascadingValue<EditContext>>(3);
builder.AddAttribute(4, "IsFixed", true); builder.AddAttribute(4, "IsFixed", true);
builder.AddAttribute(5, "Value", _fixedEditContext); builder.AddAttribute(5, "Value", _editContext);
builder.AddAttribute(6, "ChildContent", ChildContent?.Invoke(_fixedEditContext)); builder.AddAttribute(6, "ChildContent", ChildContent?.Invoke(_editContext));
builder.CloseComponent(); builder.CloseComponent();
builder.CloseElement(); builder.CloseElement();
@ -122,26 +137,26 @@ namespace Microsoft.AspNetCore.Components.Forms
private async Task HandleSubmitAsync() private async Task HandleSubmitAsync()
{ {
Debug.Assert(_fixedEditContext != null); Debug.Assert(_editContext != null);
if (OnSubmit.HasDelegate) if (OnSubmit.HasDelegate)
{ {
// When using OnSubmit, the developer takes control of the validation lifecycle // When using OnSubmit, the developer takes control of the validation lifecycle
await OnSubmit.InvokeAsync(_fixedEditContext); await OnSubmit.InvokeAsync(_editContext);
} }
else else
{ {
// Otherwise, the system implicitly runs validation on form submission // Otherwise, the system implicitly runs validation on form submission
var isValid = _fixedEditContext.Validate(); // This will likely become ValidateAsync later var isValid = _editContext.Validate(); // This will likely become ValidateAsync later
if (isValid && OnValidSubmit.HasDelegate) if (isValid && OnValidSubmit.HasDelegate)
{ {
await OnValidSubmit.InvokeAsync(_fixedEditContext); await OnValidSubmit.InvokeAsync(_editContext);
} }
if (!isValid && OnInvalidSubmit.HasDelegate) if (!isValid && OnInvalidSubmit.HasDelegate)
{ {
await OnInvalidSubmit.InvokeAsync(_fixedEditContext); await OnInvalidSubmit.InvokeAsync(_editContext);
} }
} }
} }

View File

@ -50,6 +50,12 @@ namespace Microsoft.AspNetCore.Components.Forms
/// </summary> /// </summary>
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; } [Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
/// <summary>
/// Gets or sets the display name for this field.
/// <para>This value is used when generating error messages when the input value fails to parse correctly.</para>
/// </summary>
[Parameter] public string? DisplayName { get; set; }
/// <summary> /// <summary>
/// Gets the associated <see cref="Forms.EditContext"/>. /// Gets the associated <see cref="Forms.EditContext"/>.
/// </summary> /// </summary>

View File

@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
else else
{ {
validationErrorMessage = string.Format(ParsingErrorMessage, FieldIdentifier.FieldName); validationErrorMessage = string.Format(ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
return false; return false;
} }
} }

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Components.Forms
else else
{ {
result = default; result = default;
validationErrorMessage = $"The {input.FieldIdentifier.FieldName} field is not valid."; validationErrorMessage = $"The {input.DisplayName ?? input.FieldIdentifier.FieldName} field is not valid.";
return false; return false;
} }
} }

View File

@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
else else
{ {
validationErrorMessage = string.Format(ParsingErrorMessage, FieldIdentifier.FieldName); validationErrorMessage = string.Format(ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
return false; return false;
} }
} }

View File

@ -122,6 +122,8 @@ namespace Microsoft.AspNetCore.Components.Web
[EventHandler("onpointerlockerror", typeof(EventArgs), true, true)] [EventHandler("onpointerlockerror", typeof(EventArgs), true, true)]
[EventHandler("onreadystatechange", typeof(EventArgs), true, true)] [EventHandler("onreadystatechange", typeof(EventArgs), true, true)]
[EventHandler("onscroll", typeof(EventArgs), true, true)] [EventHandler("onscroll", typeof(EventArgs), true, true)]
[EventHandler("ontoggle", typeof(EventArgs), true, true)]
public static class EventHandlers public static class EventHandlers
{ {
} }

View File

@ -0,0 +1,120 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Components.Forms
{
public class EditFormTest
{
[Fact]
public async Task ThrowsIfBothEditContextAndModelAreSupplied()
{
// Arrange
var editForm = new EditForm
{
EditContext = new EditContext(new TestModel()),
Model = new TestModel()
};
var testRenderer = new TestRenderer();
var componentId = testRenderer.AssignRootComponentId(editForm);
// Act/Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => testRenderer.RenderRootComponentAsync(componentId));
Assert.StartsWith($"{nameof(EditForm)} requires a {nameof(EditForm.Model)} parameter, or an {nameof(EditContext)} parameter, but not both.", ex.Message);
}
[Fact]
public async Task ThrowsIfBothEditContextAndModelAreNull()
{
// Arrange
var editForm = new EditForm();
var testRenderer = new TestRenderer();
var componentId = testRenderer.AssignRootComponentId(editForm);
// Act/Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => testRenderer.RenderRootComponentAsync(componentId));
Assert.StartsWith($"{nameof(EditForm)} requires either a {nameof(EditForm.Model)} parameter, or an {nameof(EditContext)} parameter, please provide one of these.", ex.Message);
}
[Fact]
public async Task ReturnsEditContextWhenModelParameterUsed()
{
// Arrange
var model = new TestModel();
var rootComponent = new TestEditFormHostComponent
{
Model = model
};
var editFormComponent = await RenderAndGetTestEditFormComponentAsync(rootComponent);
// Act
var returnedEditContext = editFormComponent.EditContext;
// Assert
Assert.NotNull(returnedEditContext);
Assert.Same(model, returnedEditContext.Model);
}
[Fact]
public async Task ReturnsEditContextWhenEditContextParameterUsed()
{
// Arrange
var editContext = new EditContext(new TestModel());
var rootComponent = new TestEditFormHostComponent
{
EditContext = editContext
};
var editFormComponent = await RenderAndGetTestEditFormComponentAsync(rootComponent);
// Act
var returnedEditContext = editFormComponent.EditContext;
// Assert
Assert.Same(editContext, returnedEditContext);
}
private static EditForm FindEditFormComponent(CapturedBatch batch)
=> batch.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Select(f => f.Component)
.OfType<EditForm>()
.Single();
private static async Task<EditForm> RenderAndGetTestEditFormComponentAsync(TestEditFormHostComponent hostComponent)
{
var testRenderer = new TestRenderer();
var componentId = testRenderer.AssignRootComponentId(hostComponent);
await testRenderer.RenderRootComponentAsync(componentId);
return FindEditFormComponent(testRenderer.Batches.Single());
}
class TestModel
{
public string StringProperty { get; set; }
}
class TestEditFormHostComponent : AutoRenderComponent
{
public EditContext EditContext { get; set; }
public TestModel Model { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenComponent<EditForm>(0);
builder.AddAttribute(1, "Model", Model);
builder.AddAttribute(2, "EditContext", EditContext);
builder.CloseComponent();
}
}
}
}

View File

@ -4,10 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit; using Xunit;
@ -35,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Forms
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model), ValueExpression = () => model.StringProperty }; var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model), ValueExpression = () => model.StringProperty };
await RenderAndGetTestInputComponentAsync(rootComponent); await InputRenderer.RenderAndGetComponent(rootComponent);
// Act/Assert // Act/Assert
rootComponent.EditContext = new EditContext(model); rootComponent.EditContext = new EditContext(model);
@ -51,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.Forms
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model) }; var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model) };
// Act/Assert // Act/Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => RenderAndGetTestInputComponentAsync(rootComponent)); var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => InputRenderer.RenderAndGetComponent(rootComponent));
Assert.Contains($"{typeof(TestInputComponent<string>)} requires a value for the 'ValueExpression' parameter. Normally this is provided automatically when using 'bind-Value'.", ex.Message); Assert.Contains($"{typeof(TestInputComponent<string>)} requires a value for the 'ValueExpression' parameter. Normally this is provided automatically when using 'bind-Value'.", ex.Message);
} }
@ -68,7 +65,7 @@ namespace Microsoft.AspNetCore.Components.Forms
}; };
// Act // Act
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Assert // Assert
Assert.Equal("some value", inputComponent.CurrentValue); Assert.Equal("some value", inputComponent.CurrentValue);
@ -87,7 +84,7 @@ namespace Microsoft.AspNetCore.Components.Forms
}; };
// Act // Act
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Assert // Assert
Assert.Same(rootComponent.EditContext, inputComponent.EditContext); Assert.Same(rootComponent.EditContext, inputComponent.EditContext);
@ -106,7 +103,7 @@ namespace Microsoft.AspNetCore.Components.Forms
}; };
// Act // Act
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Assert // Assert
Assert.Equal(FieldIdentifier.Create(() => model.StringProperty), inputComponent.FieldIdentifier); Assert.Equal(FieldIdentifier.Create(() => model.StringProperty), inputComponent.FieldIdentifier);
@ -123,7 +120,7 @@ namespace Microsoft.AspNetCore.Components.Forms
Value = "initial value", Value = "initial value",
ValueExpression = () => model.StringProperty ValueExpression = () => model.StringProperty
}; };
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Equal("initial value", inputComponent.CurrentValue); Assert.Equal("initial value", inputComponent.CurrentValue);
// Act // Act
@ -146,7 +143,7 @@ namespace Microsoft.AspNetCore.Components.Forms
ValueChanged = val => valueChangedCallLog.Add(val), ValueChanged = val => valueChangedCallLog.Add(val),
ValueExpression = () => model.StringProperty ValueExpression = () => model.StringProperty
}; };
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Empty(valueChangedCallLog); Assert.Empty(valueChangedCallLog);
// Act // Act
@ -169,7 +166,7 @@ namespace Microsoft.AspNetCore.Components.Forms
ValueChanged = val => valueChangedCallLog.Add(val), ValueChanged = val => valueChangedCallLog.Add(val),
ValueExpression = () => model.StringProperty ValueExpression = () => model.StringProperty
}; };
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Empty(valueChangedCallLog); Assert.Empty(valueChangedCallLog);
// Act // Act
@ -190,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.Forms
Value = "initial value", Value = "initial value",
ValueExpression = () => model.StringProperty ValueExpression = () => model.StringProperty
}; };
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.False(rootComponent.EditContext.IsModified(() => model.StringProperty)); Assert.False(rootComponent.EditContext.IsModified(() => model.StringProperty));
// Act // Act
@ -213,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.Forms
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty); var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
// Act/Assert: Initially, it's valid and unmodified // Act/Assert: Initially, it's valid and unmodified
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Equal("valid", inputComponent.CssClass); // no Class was specified Assert.Equal("valid", inputComponent.CssClass); // no Class was specified
// Act/Assert: Modify the field // Act/Assert: Modify the field
@ -251,7 +248,7 @@ namespace Microsoft.AspNetCore.Components.Forms
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty); var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
// Act/Assert // Act/Assert
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Equal("my-class other-class valid", inputComponent.CssClass); Assert.Equal("my-class other-class valid", inputComponent.CssClass);
// Act/Assert: Retains custom class when changing field class // Act/Assert: Retains custom class when changing field class
@ -270,7 +267,7 @@ namespace Microsoft.AspNetCore.Components.Forms
Value = new DateTime(1915, 3, 2), Value = new DateTime(1915, 3, 2),
ValueExpression = () => model.DateProperty ValueExpression = () => model.DateProperty
}; };
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act/Assert // Act/Assert
Assert.Equal("1915/03/02", inputComponent.CurrentValueAsString); Assert.Equal("1915/03/02", inputComponent.CurrentValueAsString);
@ -289,7 +286,7 @@ namespace Microsoft.AspNetCore.Components.Forms
ValueExpression = () => model.DateProperty ValueExpression = () => model.DateProperty
}; };
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty); var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
var numValidationStateChanges = 0; var numValidationStateChanges = 0;
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; }; rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
@ -319,7 +316,7 @@ namespace Microsoft.AspNetCore.Components.Forms
ValueExpression = () => model.DateProperty ValueExpression = () => model.DateProperty
}; };
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty); var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
var numValidationStateChanges = 0; var numValidationStateChanges = 0;
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; }; rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
@ -470,21 +467,6 @@ namespace Microsoft.AspNetCore.Components.Forms
Assert.Equal("userSpecifiedValue", component.AdditionalAttributes["aria-invalid"]); Assert.Equal("userSpecifiedValue", component.AdditionalAttributes["aria-invalid"]);
} }
private static TComponent FindComponent<TComponent>(CapturedBatch batch)
=> batch.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Select(f => f.Component)
.OfType<TComponent>()
.Single();
private static async Task<TComponent> RenderAndGetTestInputComponentAsync<TValue, TComponent>(TestInputHostComponent<TValue, TComponent> hostComponent) where TComponent : TestInputComponent<TValue>
{
var testRenderer = new TestRenderer();
var componentId = testRenderer.AssignRootComponentId(hostComponent);
await testRenderer.RenderRootComponentAsync(componentId);
return FindComponent<TComponent>(testRenderer.Batches.Single());
}
class TestModel class TestModel
{ {
public string StringProperty { get; set; } public string StringProperty { get; set; }
@ -530,7 +512,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
} }
class TestDateInputComponent : TestInputComponent<DateTime> private class TestDateInputComponent : TestInputComponent<DateTime>
{ {
protected override string FormatValueAsString(DateTime value) protected override string FormatValueAsString(DateTime value)
=> value.ToString("yyyy/MM/dd"); => value.ToString("yyyy/MM/dd");
@ -549,35 +531,5 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
} }
} }
class TestInputHostComponent<TValue, TComponent> : AutoRenderComponent where TComponent : TestInputComponent<TValue>
{
public Dictionary<string, object> AdditionalAttributes { get; set; }
public EditContext EditContext { get; set; }
public TValue Value { get; set; }
public Action<TValue> ValueChanged { get; set; }
public Expression<Func<TValue>> ValueExpression { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenComponent<CascadingValue<EditContext>>(0);
builder.AddAttribute(1, "Value", EditContext);
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<TComponent>(0);
childBuilder.AddAttribute(0, "Value", Value);
childBuilder.AddAttribute(1, "ValueChanged",
EventCallback.Factory.Create(this, ValueChanged));
childBuilder.AddAttribute(2, "ValueExpression", ValueExpression);
childBuilder.AddMultipleAttributes(3, AdditionalAttributes);
childBuilder.CloseComponent();
}));
builder.CloseComponent();
}
}
} }
} }

View File

@ -0,0 +1,56 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Components.Forms
{
public class InputDateTest
{
[Fact]
public async Task ValidationErrorUsesDisplayAttributeName()
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputHostComponent<DateTime, TestInputDateComponent>
{
EditContext = new EditContext(model),
ValueExpression = () => model.DateProperty,
AdditionalAttributes = new Dictionary<string, object>
{
{ "DisplayName", "Date property" }
}
};
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
await inputComponent.SetCurrentValueAsStringAsync("invalidDate");
// Assert
var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier);
Assert.NotEmpty(validationMessages);
Assert.Contains("The Date property field must be a date.", validationMessages);
}
private class TestModel
{
public DateTime DateProperty { get; set; }
}
private class TestInputDateComponent : InputDate<DateTime>
{
public async Task SetCurrentValueAsStringAsync(string value)
{
// This is equivalent to the subclass writing to CurrentValueAsString
// (e.g., from @bind), except to simplify the test code there's an InvokeAsync
// here. In production code it wouldn't normally be required because @bind
// calls run on the sync context anyway.
await InvokeAsync(() => { base.CurrentValueAsString = value; });
}
}
}
}

View File

@ -0,0 +1,55 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Components.Forms
{
public class InputNumberTest
{
[Fact]
public async Task ValidationErrorUsesDisplayAttributeName()
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputHostComponent<int, TestInputNumberComponent>
{
EditContext = new EditContext(model),
ValueExpression = () => model.SomeNumber,
AdditionalAttributes = new Dictionary<string, object>
{
{ "DisplayName", "Some number" }
}
};
var fieldIdentifier = FieldIdentifier.Create(() => model.SomeNumber);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
await inputComponent.SetCurrentValueAsStringAsync("notANumber");
// Assert
var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier);
Assert.NotEmpty(validationMessages);
Assert.Contains("The Some number field must be a number.", validationMessages);
}
private class TestModel
{
public int SomeNumber { get; set; }
}
private class TestInputNumberComponent : InputNumber<int>
{
public async Task SetCurrentValueAsStringAsync(string value)
{
// This is equivalent to the subclass writing to CurrentValueAsString
// (e.g., from @bind), except to simplify the test code there's an InvokeAsync
// here. In production code it wouldn't normally be required because @bind
// calls run on the sync context anyway.
await InvokeAsync(() => { base.CurrentValueAsString = value; });
}
}
}
}

View File

@ -0,0 +1,29 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
namespace Microsoft.AspNetCore.Components.Forms
{
internal static class InputRenderer
{
public static async Task<TComponent> RenderAndGetComponent<TValue, TComponent>(TestInputHostComponent<TValue, TComponent> hostComponent)
where TComponent : InputBase<TValue>
{
var testRenderer = new TestRenderer();
var componentId = testRenderer.AssignRootComponentId(hostComponent);
await testRenderer.RenderRootComponentAsync(componentId);
return FindComponent<TComponent>(testRenderer.Batches.Single());
}
private static TComponent FindComponent<TComponent>(CapturedBatch batch)
=> batch.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Select(f => f.Component)
.OfType<TComponent>()
.Single();
}
}

View File

@ -2,12 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Linq; using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Components.Forms namespace Microsoft.AspNetCore.Components.Forms
@ -19,12 +15,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<TestEnum> var rootComponent = new TestInputHostComponent<TestEnum, TestInputSelect<TestEnum>>
{ {
EditContext = new EditContext(model), EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableEnum ValueExpression = () => model.NotNullableEnum
}; };
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act // Act
inputSelectComponent.CurrentValueAsString = "Two"; inputSelectComponent.CurrentValueAsString = "Two";
@ -38,12 +34,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<TestEnum> var rootComponent = new TestInputHostComponent<TestEnum, TestInputSelect<TestEnum>>
{ {
EditContext = new EditContext(model), EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableEnum ValueExpression = () => model.NotNullableEnum
}; };
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act // Act
inputSelectComponent.CurrentValueAsString = ""; inputSelectComponent.CurrentValueAsString = "";
@ -57,12 +53,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<TestEnum?> var rootComponent = new TestInputHostComponent<TestEnum?, TestInputSelect<TestEnum?>>
{ {
EditContext = new EditContext(model), EditContext = new EditContext(model),
ValueExpression = () => model.NullableEnum ValueExpression = () => model.NullableEnum
}; };
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act // Act
inputSelectComponent.CurrentValueAsString = "Two"; inputSelectComponent.CurrentValueAsString = "Two";
@ -76,12 +72,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<TestEnum?> var rootComponent = new TestInputHostComponent<TestEnum?, TestInputSelect<TestEnum?>>
{ {
EditContext = new EditContext(model), EditContext = new EditContext(model),
ValueExpression = () => model.NullableEnum ValueExpression = () => model.NullableEnum
}; };
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act // Act
inputSelectComponent.CurrentValueAsString = ""; inputSelectComponent.CurrentValueAsString = "";
@ -96,12 +92,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<Guid> var rootComponent = new TestInputHostComponent<Guid, TestInputSelect<Guid>>
{ {
EditContext = new EditContext(model), EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableGuid ValueExpression = () => model.NotNullableGuid
}; };
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act // Act
var guid = Guid.NewGuid(); var guid = Guid.NewGuid();
@ -117,12 +113,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<Guid?> var rootComponent = new TestInputHostComponent<Guid?, TestInputSelect<Guid?>>
{ {
EditContext = new EditContext(model), EditContext = new EditContext(model),
ValueExpression = () => model.NullableGuid ValueExpression = () => model.NullableGuid
}; };
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act // Act
var guid = Guid.NewGuid(); var guid = Guid.NewGuid();
@ -138,12 +134,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<int> var rootComponent = new TestInputHostComponent<int, TestInputSelect<int>>
{ {
EditContext = new EditContext(model), EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableInt ValueExpression = () => model.NotNullableInt
}; };
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act // Act
inputSelectComponent.CurrentValueAsString = "42"; inputSelectComponent.CurrentValueAsString = "42";
@ -158,12 +154,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
// Arrange // Arrange
var model = new TestModel(); var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<int?> var rootComponent = new TestInputHostComponent<int?, TestInputSelect<int?>>
{ {
EditContext = new EditContext(model), EditContext = new EditContext(model),
ValueExpression = () => model.NullableInt ValueExpression = () => model.NullableInt
}; };
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act // Act
inputSelectComponent.CurrentValueAsString = "42"; inputSelectComponent.CurrentValueAsString = "42";
@ -172,19 +168,30 @@ namespace Microsoft.AspNetCore.Components.Forms
Assert.Equal(42, inputSelectComponent.CurrentValue); Assert.Equal(42, inputSelectComponent.CurrentValue);
} }
private static TestInputSelect<TValue> FindInputSelectComponent<TValue>(CapturedBatch batch) [Fact]
=> batch.ReferenceFrames public async Task ValidationErrorUsesDisplayAttributeName()
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Select(f => f.Component)
.OfType<TestInputSelect<TValue>>()
.Single();
private static async Task<TestInputSelect<TValue>> RenderAndGetTestInputComponentAsync<TValue>(TestInputSelectHostComponent<TValue> hostComponent)
{ {
var testRenderer = new TestRenderer(); // Arrange
var componentId = testRenderer.AssignRootComponentId(hostComponent); var model = new TestModel();
await testRenderer.RenderRootComponentAsync(componentId); var rootComponent = new TestInputHostComponent<int, TestInputSelect<int>>
return FindInputSelectComponent<TValue>(testRenderer.Batches.Single()); {
EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableInt,
AdditionalAttributes = new Dictionary<string, object>
{
{ "DisplayName", "Some number" }
}
};
var fieldIdentifier = FieldIdentifier.Create(() => model.NotNullableInt);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
await inputSelectComponent.SetCurrentValueAsStringAsync("invalidNumber");
// Assert
var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier);
Assert.NotEmpty(validationMessages);
Assert.Contains("The Some number field is not valid.", validationMessages);
} }
enum TestEnum enum TestEnum
@ -218,25 +225,13 @@ namespace Microsoft.AspNetCore.Components.Forms
get => base.CurrentValueAsString; get => base.CurrentValueAsString;
set => base.CurrentValueAsString = value; set => base.CurrentValueAsString = value;
} }
} public async Task SetCurrentValueAsStringAsync(string value)
class TestInputSelectHostComponent<TValue> : AutoRenderComponent
{
public EditContext EditContext { get; set; }
public Expression<Func<TValue>> ValueExpression { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{ {
builder.OpenComponent<CascadingValue<EditContext>>(0); // This is equivalent to the subclass writing to CurrentValueAsString
builder.AddAttribute(1, "Value", EditContext); // (e.g., from @bind), except to simplify the test code there's an InvokeAsync
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder => // here. In production code it wouldn't normally be required because @bind
{ // calls run on the sync context anyway.
childBuilder.OpenComponent<TestInputSelect<TValue>>(0); await InvokeAsync(() => { base.CurrentValueAsString = value; });
childBuilder.AddAttribute(0, "ValueExpression", ValueExpression);
childBuilder.CloseComponent();
}));
builder.CloseComponent();
} }
} }
} }

View File

@ -0,0 +1,41 @@
// 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.Expressions;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Test.Helpers;
namespace Microsoft.AspNetCore.Components.Forms
{
internal class TestInputHostComponent<TValue, TComponent> : AutoRenderComponent where TComponent : InputBase<TValue>
{
public Dictionary<string, object> AdditionalAttributes { get; set; }
public EditContext EditContext { get; set; }
public TValue Value { get; set; }
public Action<TValue> ValueChanged { get; set; }
public Expression<Func<TValue>> ValueExpression { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenComponent<CascadingValue<EditContext>>(0);
builder.AddAttribute(1, "Value", EditContext);
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<TComponent>(0);
childBuilder.AddAttribute(0, "Value", Value);
childBuilder.AddAttribute(1, "ValueChanged",
EventCallback.Factory.Create(this, ValueChanged));
childBuilder.AddAttribute(2, "ValueExpression", ValueExpression);
childBuilder.AddMultipleAttributes(3, AdditionalAttributes);
childBuilder.CloseComponent();
}));
builder.CloseComponent();
}
}
}

View File

@ -44,6 +44,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Browser.Equal("1", () => Browser.FindElement(By.Id("count")).Text); Browser.Equal("1", () => Browser.FindElement(By.Id("count")).Text);
} }
[Fact]
public void PrerenderingWaitsForAsyncDisposableComponents()
{
Navigate("/prerendered/prerendered-async-disposal");
// Prerendered output shows "not connected"
Browser.Equal("After async disposal", () => Browser.FindElement(By.Id("disposal-message")).Text);
}
[Fact] [Fact]
public void CanUseJSInteropFromOnAfterRenderAsync() public void CanUseJSInteropFromOnAfterRenderAsync()
{ {

View File

@ -216,6 +216,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("Fourth", () => boundValue.Text); Browser.Equal("Fourth", () => boundValue.Text);
Assert.Equal("Fourth choice", target.SelectedOption.Text); Assert.Equal("Fourth choice", target.SelectedOption.Text);
// verify that changing an option value and selected value at the same time works.
Browser.FindElement(By.Id("change-variable-value")).Click();
Browser.Equal("Sixth", () => boundValue.Text);
// Verify we can select options whose value is empty // Verify we can select options whose value is empty
// https://github.com/dotnet/aspnetcore/issues/17735 // https://github.com/dotnet/aspnetcore/issues/17735
target.SelectByText("Empty value"); target.SelectByText("Empty value");

View File

@ -115,6 +115,24 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("onmousedown,onmouseup,", () => output.Text); Browser.Equal("onmousedown,onmouseup,", () => output.Text);
} }
[Fact]
public void Toggle_CanTrigger()
{
Browser.MountTestComponent<ToggleEventComponent>();
var detailsToggle = Browser.FindElement(By.Id("details-toggle"));
var output = Browser.FindElement(By.Id("output"));
Assert.Equal(string.Empty, output.Text);
// Click
var actions = new Actions(Browser).Click(detailsToggle);
actions.Perform();
Browser.Equal("ontoggle,", () => output.Text);
}
[Fact] [Fact]
public void PointerDown_CanTrigger() public void PointerDown_CanTrigger()
{ {
@ -270,6 +288,17 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Contains(expectedMessage, () => errorLog.Text); Browser.Contains(expectedMessage, () => errorLog.Text);
} }
[Fact]
public void RenderAttributesBeforeConnectedCallBack()
{
Browser.MountTestComponent<RenderAttributesBeforeConnectedCallback>();
var element = Browser.FindElement(By.TagName("custom-web-component-data-from-attribute"));
var expectedContent = "success";
Browser.Contains(expectedContent, () => element.Text);
}
void SendKeysSequentially(IWebElement target, string text) void SendKeysSequentially(IWebElement target, string text)
{ {
// Calling it for each character works around some chars being skipped // Calling it for each character works around some chars being skipped

View File

@ -109,6 +109,17 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal(expected, app.FindElement(By.Id("test-info")).Text); Assert.Equal(expected, app.FindElement(By.Id("test-info")).Text);
} }
[Fact]
public void CanArriveAtPageWithCatchAllParameter()
{
SetUrlViaPushState("/WithCatchAllParameter/life/the/universe/and/everything%20%3D%2042");
var app = Browser.MountTestComponent<TestRouter>();
var expected = $"The answer: life/the/universe/and/everything = 42.";
Assert.Equal(expected, app.FindElement(By.Id("test-info")).Text);
}
[Fact] [Fact]
public void CanArriveAtNonDefaultPage() public void CanArriveAtNonDefaultPage()
{ {

View File

@ -0,0 +1,11 @@
@implements IAsyncDisposable
@code{
[Parameter] public EventCallback<string> SetMessage { get; set; }
public async ValueTask DisposeAsync()
{
await SetMessage.InvokeAsync("Before async disposal");
await Task.Yield();
await SetMessage.InvokeAsync("After async disposal");
}
}

View File

@ -0,0 +1,25 @@
@page "/prerendered-async-disposal"
<p>
This component shows that prerendering will work for components that implement IAsyncDisposable to finish
disposing before rendering the output as html.
</p>
<p id="disposal-message">@_message</p>
@if (!_hideComponent)
{
<AsyncDisposableComponent SetMessage="@SetMessage" />
}
@code {
private bool _hideComponent = false;
private string _message = "Uninitialized";
protected override async Task OnInitializedAsync()
{
await Task.Yield();
_hideComponent = true;
}
private void SetMessage(string message) => _message = message;
}

View File

@ -242,6 +242,7 @@
<optgroup label="Some choices"> <optgroup label="Some choices">
<!-- Show it also works with optgroup --> <!-- Show it also works with optgroup -->
<option value=@string.Empty>Empty value</option> <option value=@string.Empty>Empty value</option>
<option value="@variableValue">Variable value</option>
<option value=@SelectableValue.First>First choice</option> <option value=@SelectableValue.First>First choice</option>
<option value=@SelectableValue.Second>Second choice</option> <option value=@SelectableValue.Second>Second choice</option>
<option value=@SelectableValue.Third>Third choice</option> <option value=@SelectableValue.Third>Third choice</option>
@ -253,6 +254,7 @@
</select> </select>
<span id="select-box-value">@selectValue</span> <span id="select-box-value">@selectValue</span>
<button id="select-box-add-option" @onclick="AddAndSelectNewSelectOption">Add and select new item</button> <button id="select-box-add-option" @onclick="AddAndSelectNewSelectOption">Add and select new item</button>
<button id="change-variable-value" @onclick="ChangeVariableValueToSixth">Change variable value to Sixth.</button>
</p> </p>
<h2>Select (markup block)</h2> <h2>Select (markup block)</h2>
@ -383,13 +385,21 @@
DateTime? timeStepTextboxNullableDateTimeValue = null; DateTime? timeStepTextboxNullableDateTimeValue = null;
bool includeFourthOption = false; bool includeFourthOption = false;
enum SelectableValue { First, Second, Third, Fourth } enum SelectableValue { First, Second, Third, Fourth, Fifth, Sixth }
SelectableValue? selectValue = SelectableValue.Second; SelectableValue? selectValue = SelectableValue.Second;
SelectableValue? selectMarkupValue = SelectableValue.Second; SelectableValue? selectMarkupValue = SelectableValue.Second;
SelectableValue variableValue = SelectableValue.Fifth;
void AddAndSelectNewSelectOption() void AddAndSelectNewSelectOption()
{ {
includeFourthOption = true; includeFourthOption = true;
selectValue = SelectableValue.Fourth; selectValue = SelectableValue.Fourth;
} }
void ChangeVariableValueToSixth()
{
selectValue = SelectableValue.Sixth;
variableValue = SelectableValue.Sixth;
}
} }

View File

@ -70,6 +70,7 @@
<option value="BasicTestApp.RedTextComponent">Red text</option> <option value="BasicTestApp.RedTextComponent">Red text</option>
<option value="BasicTestApp.ReliabilityComponent">Server reliability component</option> <option value="BasicTestApp.ReliabilityComponent">Server reliability component</option>
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option> <option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
<option value="BasicTestApp.RenderAttributesBeforeConnectedCallback">Render attributes before ConnectedCallback</option>
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option> <option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
<option value="BasicTestApp.RouterTest.NavigationManagerComponent">NavigationManager Test</option> <option value="BasicTestApp.RouterTest.NavigationManagerComponent">NavigationManager Test</option>
<option value="BasicTestApp.RouterTest.TestRouter">Router</option> <option value="BasicTestApp.RouterTest.TestRouter">Router</option>
@ -82,6 +83,7 @@
<option value="BasicTestApp.TextOnlyComponent">Plain text</option> <option value="BasicTestApp.TextOnlyComponent">Plain text</option>
<option value="BasicTestApp.TouchEventComponent">Touch events</option> <option value="BasicTestApp.TouchEventComponent">Touch events</option>
<option value="BasicTestApp.SelectVariantsComponent">Select with component options</option> <option value="BasicTestApp.SelectVariantsComponent">Select with component options</option>
<option value="BasicTestApp.ToggleEventComponent">Toggle Event</option>
</select> </select>
<span id="runtime-info"><code><tt>@System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription</tt></code></span> <span id="runtime-info"><code><tt>@System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription</tt></code></span>

View File

@ -0,0 +1,14 @@
<h1>Render Attributes Before connectedCallback</h1>
<p>
When the connectedcallback event fires, it's nice to have the attributes of the HTML element set already,
as data is usually passed to the component using custom attributes.
</p>
<custom-web-component-data-from-attribute myattribute="@attributeString"></custom-web-component-data-from-attribute>
@code {
string attributeString = "success";
}

View File

@ -0,0 +1,7 @@
@page "/WithCatchAllParameter/{*theAnswer}"
<div id="test-info">The answer: @TheAnswer.</div>
@code
{
[Parameter] public string TheAnswer { get; set; }
}

View File

@ -0,0 +1,25 @@
<div>
<p>
Output: <span id="output">@message</span>
</p>
<p>Open the details.</p>
<details id="details-toggle" @ontoggle="OnToggle">
<summary>Summary</summary>
<p></p>
<p>Detailed content</p>
</details>
</div>
@code {
string message { get; set; }
void OnToggle(EventArgs e)
{
message += "ontoggle,";
StateHasChanged();
}
}

View File

@ -23,6 +23,7 @@
<!-- Used for specific test cases --> <!-- Used for specific test cases -->
<script src="js/jsinteroptests.js"></script> <script src="js/jsinteroptests.js"></script>
<script src="js/renderattributestest.js"></script>
<script src="js/webComponentPerformingJsInterop.js"></script> <script src="js/webComponentPerformingJsInterop.js"></script>
<script> <script>

View File

@ -0,0 +1,9 @@
// This web component is used from the RenderAttributesBeforeConnectedCallback test case
window.customElements.define('custom-web-component-data-from-attribute', class extends HTMLElement {
connectedCallback() {
let myattribute = this.getAttribute('myattribute') || 'failed';
this.innerHTML = myattribute;
}
});

View File

@ -16,6 +16,7 @@
<!-- Used for specific test cases --> <!-- Used for specific test cases -->
<script src="js/jsinteroptests.js"></script> <script src="js/jsinteroptests.js"></script>
<script src="js/renderattributestest.js"></script>
<script src="js/webComponentPerformingJsInterop.js"></script> <script src="js/webComponentPerformingJsInterop.js"></script>
<div id="blazor-error-ui"> <div id="blazor-error-ui">

View File

@ -19,9 +19,10 @@ namespace Microsoft.AspNetCore.Hosting
/// <param name="timeout">The timeout for stopping gracefully. Once expired the /// <param name="timeout">The timeout for stopping gracefully. Once expired the
/// server may terminate any remaining active connections.</param> /// server may terminate any remaining active connections.</param>
/// <returns>A <see cref="Task"/> that completes when the <see cref="IWebHost"/> stops.</returns> /// <returns>A <see cref="Task"/> that completes when the <see cref="IWebHost"/> stops.</returns>
public static Task StopAsync(this IWebHost host, TimeSpan timeout) public static async Task StopAsync(this IWebHost host, TimeSpan timeout)
{ {
return host.StopAsync(new CancellationTokenSource(timeout).Token); using var cts = new CancellationTokenSource(timeout);
await host.StopAsync(cts.Token);
} }
/// <summary> /// <summary>

View File

@ -198,7 +198,6 @@ namespace Microsoft.AspNetCore.Hosting
} }
[ConditionalFact] [ConditionalFact]
[QuarantinedTest]
public async Task WebHostStopAsyncUsesDefaultTimeoutIfGivenTokenDoesNotFire() public async Task WebHostStopAsyncUsesDefaultTimeoutIfGivenTokenDoesNotFire()
{ {
var data = new Dictionary<string, string> var data = new Dictionary<string, string>
@ -313,7 +312,6 @@ namespace Microsoft.AspNetCore.Hosting
} }
[ConditionalFact] [ConditionalFact]
[QuarantinedTest]
public void WebHostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown() public void WebHostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()

View File

@ -111,7 +111,7 @@ namespace Microsoft.AspNetCore.Http
} }
/// <summary> /// <summary>
/// This calls StartAsync if it has not previoulsy been called. /// This calls StartAsync if it has not previously been called.
/// It will complete the adapted pipe if it exists. /// It will complete the adapted pipe if it exists.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
@ -128,6 +128,11 @@ namespace Microsoft.AspNetCore.Http
return; return;
} }
if (!_started)
{
await StartAsync();
}
_completed = true; _completed = true;
if (_pipeWriter != null) if (_pipeWriter != null)

View File

@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Buffers;
using System.IO;
using System.IO.Pipelines;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Http.Features
{
public class StreamResponseBodyFeatureTests
{
[Fact]
public async Task CompleteAsyncCallsStartAsync()
{
// Arrange
var stream = new MemoryStream();
var streamResponseBodyFeature = new TestStreamResponseBodyFeature(stream);
// Act
await streamResponseBodyFeature.CompleteAsync();
//Assert
Assert.Equal(1, streamResponseBodyFeature.StartCalled);
}
[Fact]
public async Task CompleteAsyncWontCallsStartAsyncIfAlreadyStarted()
{
// Arrange
var stream = new MemoryStream();
var streamResponseBodyFeature = new TestStreamResponseBodyFeature(stream);
await streamResponseBodyFeature.StartAsync();
// Act
await streamResponseBodyFeature.CompleteAsync();
//Assert
Assert.Equal(1, streamResponseBodyFeature.StartCalled);
}
}
public class TestStreamResponseBodyFeature : StreamResponseBodyFeature
{
public TestStreamResponseBodyFeature(Stream stream)
: base(stream)
{
}
public override Task StartAsync(CancellationToken cancellationToken = default)
{
StartCalled++;
return base.StartAsync(cancellationToken);
}
public int StartCalled { get; private set; }
}
}

View File

@ -17,7 +17,7 @@
<button class="btn btn-primary" type="submit">Download</button> <button class="btn btn-primary" type="submit">Download</button>
</form> </form>
<p> <p>
<a id="delete" asp-page="DeletePersonalData" class="btn btn-primary">Delete</a> <a id="delete" asp-page="DeletePersonalData" class="btn btn-secondary">Delete</a>
</p> </p>
</div> </div>
</div> </div>

View File

@ -132,6 +132,7 @@ namespace Templates.Test
} }
[Fact] [Fact]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23992")]
public async Task BlazorWasmStandalonePwaTemplate_Works() public async Task BlazorWasmStandalonePwaTemplate_Works()
{ {
var project = await ProjectFactory.GetOrCreateProject("blazorstandalonepwa", Output); var project = await ProjectFactory.GetOrCreateProject("blazorstandalonepwa", Output);
@ -251,6 +252,7 @@ namespace Templates.Test
} }
[ConditionalFact] [ConditionalFact]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23992")]
// LocalDB doesn't work on non Windows platforms // LocalDB doesn't work on non Windows platforms
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
public Task BlazorWasmHostedTemplate_IndividualAuth_Works_WithLocalDB() public Task BlazorWasmHostedTemplate_IndividualAuth_Works_WithLocalDB()
@ -259,6 +261,7 @@ namespace Templates.Test
} }
[Fact] [Fact]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23992")]
public Task BlazorWasmHostedTemplate_IndividualAuth_Works_WithOutLocalDB() public Task BlazorWasmHostedTemplate_IndividualAuth_Works_WithOutLocalDB()
{ {
return BlazorWasmHostedTemplate_IndividualAuth_Works(false); return BlazorWasmHostedTemplate_IndividualAuth_Works(false);

View File

@ -24,6 +24,7 @@ app {
height: 3.5rem; height: 3.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
z-index: 10;
} }
.main { .main {

View File

@ -24,6 +24,7 @@ app {
height: 3.5rem; height: 3.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
z-index: 10;
} }
.main { .main {

View File

@ -71,6 +71,7 @@ namespace Templates.Test
[Theory] [Theory]
[MemberData(nameof(TemplateBaselines))] [MemberData(nameof(TemplateBaselines))]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23993")]
public async Task Template_Produces_The_Right_Set_Of_FilesAsync(string arguments, string[] expectedFiles) public async Task Template_Produces_The_Right_Set_Of_FilesAsync(string arguments, string[] expectedFiles)
{ {
Project = await ProjectFactory.GetOrCreateProject("baseline" + SanitizeArgs(arguments), Output); Project = await ProjectFactory.GetOrCreateProject("baseline" + SanitizeArgs(arguments), Output);
@ -151,4 +152,4 @@ namespace Templates.Test
} }
} }
} }
} }

View File

@ -223,6 +223,7 @@ namespace Templates.Test
} }
[Fact] [Fact]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23993")]
public async Task MvcTemplate_RazorRuntimeCompilation_BuildsAndPublishes() public async Task MvcTemplate_RazorRuntimeCompilation_BuildsAndPublishes()
{ {
Project = await ProjectFactory.GetOrCreateProject("mvc_rc", Output); Project = await ProjectFactory.GetOrCreateProject("mvc_rc", Output);

View File

@ -82,6 +82,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
} }
[ConditionalFact] [ConditionalFact]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/18543")]
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
[InitializeTestProject("AppWithPackageAndP2PReferenceAndRID", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })] [InitializeTestProject("AppWithPackageAndP2PReferenceAndRID", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })]
public async Task Publish_CopiesStaticWebAssetsToDestinationFolder_PublishSingleFile() public async Task Publish_CopiesStaticWebAssetsToDestinationFolder_PublishSingleFile()

View File

@ -109,7 +109,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
[Theory] [Theory]
[MemberData(nameof(LargeUploadData))] [MemberData(nameof(LargeUploadData))]
[QuarantinedTest] [QuarantinedTest] // This is inherently flaky and should never be unquarantined.
public async Task LargeUpload(long? maxRequestBufferSize, bool connectionAdapter, bool expectPause) public async Task LargeUpload(long? maxRequestBufferSize, bool connectionAdapter, bool expectPause)
{ {
// Parameters // Parameters

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components
}; };
// This setting is not configurable, but realistically we don't expect an app to take more than 30 seconds from when // This setting is not configurable, but realistically we don't expect an app to take more than 30 seconds from when
// it got rendrered to when the circuit got started, and having an expiration on the serialized server-components helps // it got rendered to when the circuit got started, and having an expiration on the serialized server-components helps
// prevent old payloads from being replayed. // prevent old payloads from being replayed.
public static readonly TimeSpan DataExpiration = TimeSpan.FromMinutes(5); public static readonly TimeSpan DataExpiration = TimeSpan.FromMinutes(5);
} }

View File

@ -49,10 +49,15 @@ namespace Microsoft.AspNetCore.Internal
_exited = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); _exited = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
// We greedily create a timeout exception message even though a timeout is unlikely to happen for two reasons:
// 1. To make it less likely for Process getters to throw exceptions like "System.InvalidOperationException: Process has exited, ..."
// 2. To ensure if/when exceptions are thrown from Process getters, these exceptions can easily be observed.
var timeoutExMessage = $"Process proc {proc.ProcessName} {proc.StartInfo.Arguments} timed out after {DefaultProcessTimeout}.";
_processTimeoutCts = new CancellationTokenSource(timeout); _processTimeoutCts = new CancellationTokenSource(timeout);
_processTimeoutCts.Token.Register(() => _processTimeoutCts.Token.Register(() =>
{ {
_exited.TrySetException(new TimeoutException($"Process proc {proc.ProcessName} {proc.StartInfo.Arguments} timed out after {DefaultProcessTimeout}.")); _exited.TrySetException(new TimeoutException(timeoutExMessage));
}); });
} }

View File

@ -93,7 +93,7 @@ namespace System.Net.Quic.Implementations.Mock
int bytesRead = 0; int bytesRead = 0;
do do
{ {
bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None).ConfigureAwait(false); bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None, cancellationToken).ConfigureAwait(false);
} while (bytesRead != buffer.Length); } while (bytesRead != buffer.Length);
int peerListenPort = BinaryPrimitives.ReadInt32LittleEndian(buffer); int peerListenPort = BinaryPrimitives.ReadInt32LittleEndian(buffer);
@ -163,7 +163,7 @@ namespace System.Net.Quic.Implementations.Mock
int bytesRead = 0; int bytesRead = 0;
do do
{ {
bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None).ConfigureAwait(false); bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None, cancellationToken).ConfigureAwait(false);
} while (bytesRead != buffer.Length); } while (bytesRead != buffer.Length);
long streamId = BinaryPrimitives.ReadInt64LittleEndian(buffer); long streamId = BinaryPrimitives.ReadInt64LittleEndian(buffer);

View File

@ -45,7 +45,7 @@ namespace System.Net.Quic.Implementations.Mock
int bytesRead = 0; int bytesRead = 0;
do do
{ {
bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None).ConfigureAwait(false); bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None, cancellationToken).ConfigureAwait(false);
} while (bytesRead != buffer.Length); } while (bytesRead != buffer.Length);
int peerListenPort = BinaryPrimitives.ReadInt32LittleEndian(buffer); int peerListenPort = BinaryPrimitives.ReadInt32LittleEndian(buffer);

View File

@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
message: new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }), message: new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }),
binary: "lgGAwKZUYXJnZXSRKpGrX190ZXN0X2lkX18="), binary: "lgGAwKZUYXJnZXSRKpGrX190ZXN0X2lkX18="),
new ProtocolTestData( new ProtocolTestData(
name: "InvocationWithMulitpleStreams", name: "InvocationWithMultipleStreams",
message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }), message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }),
binary: "lgGAwKZUYXJnZXSQkqtfX3Rlc3RfaWRfX6xfX3Rlc3RfaWQyX18="), binary: "lgGAwKZUYXJnZXSQkqtfX3Rlc3RfaWRfX6xfX3Rlc3RfaWQyX18="),

View File

@ -20,6 +20,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
} }
[ConditionalFact] [ConditionalFact]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23994")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/23360", Queues = "Windows.10.Arm64;Windows.10.Arm64.Open;Debian.9.Arm64;Debian.9.Arm64.Open;(Debian.9.Arm64.Open)Ubuntu.1804.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036;(Debian.9.Arm64)Ubuntu.1804.Armarch@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036")] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/23360", Queues = "Windows.10.Arm64;Windows.10.Arm64.Open;Debian.9.Arm64;Debian.9.Arm64.Open;(Debian.9.Arm64.Open)Ubuntu.1804.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036;(Debian.9.Arm64)Ubuntu.1804.Armarch@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036")]
public async Task ChangeFileInDependency() public async Task ChangeFileInDependency()
{ {

View File

@ -15,7 +15,10 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{ {
public class GlobbingAppTests : IDisposable public class GlobbingAppTests : IDisposable
{ {
private GlobbingApp _app; private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);
private readonly GlobbingApp _app;
public GlobbingAppTests(ITestOutputHelper logger) public GlobbingAppTests(ITestOutputHelper logger)
{ {
_app = new GlobbingApp(logger); _app = new GlobbingApp(logger);
@ -28,17 +31,17 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
public async Task ChangeCompiledFile(bool usePollingWatcher) public async Task ChangeCompiledFile(bool usePollingWatcher)
{ {
_app.UsePollingWatcher = usePollingWatcher; _app.UsePollingWatcher = usePollingWatcher;
await _app.StartWatcherAsync(); await _app.StartWatcherAsync().TimeoutAfter(DefaultTimeout);
var types = await _app.GetCompiledAppDefinedTypes(); var types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
var programCs = File.ReadAllText(fileToChange); var programCs = File.ReadAllText(fileToChange);
File.WriteAllText(fileToChange, programCs); File.WriteAllText(fileToChange, programCs);
await _app.HasRestarted(); await _app.HasRestarted().TimeoutAfter(DefaultTimeout);
types = await _app.GetCompiledAppDefinedTypes(); types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
} }
@ -46,16 +49,16 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/23360", Queues = "Debian.9.Arm64;Debian.9.Arm64.Open;(Debian.9.Arm64.Open)Ubuntu.1804.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036;(Debian.9.Arm64)Ubuntu.1804.Armarch@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036")] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/23360", Queues = "Debian.9.Arm64;Debian.9.Arm64.Open;(Debian.9.Arm64.Open)Ubuntu.1804.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036;(Debian.9.Arm64)Ubuntu.1804.Armarch@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036")]
public async Task DeleteCompiledFile() public async Task DeleteCompiledFile()
{ {
await _app.StartWatcherAsync(); await _app.StartWatcherAsync().TimeoutAfter(DefaultTimeout);
var types = await _app.GetCompiledAppDefinedTypes(); var types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
File.Delete(fileToChange); File.Delete(fileToChange);
await _app.HasRestarted(); await _app.HasRestarted().TimeoutAfter(DefaultTimeout);
types = await _app.GetCompiledAppDefinedTypes(); types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(1, types); Assert.Equal(1, types);
} }
@ -63,16 +66,16 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/23360", Queues = "Debian.9.Arm64;Debian.9.Arm64.Open;(Debian.9.Arm64.Open)Ubuntu.1804.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036;(Debian.9.Arm64)Ubuntu.1804.Armarch@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036")] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/23360", Queues = "Debian.9.Arm64;Debian.9.Arm64.Open;(Debian.9.Arm64.Open)Ubuntu.1804.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036;(Debian.9.Arm64)Ubuntu.1804.Armarch@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036")]
public async Task DeleteSourceFolder() public async Task DeleteSourceFolder()
{ {
await _app.StartWatcherAsync(); await _app.StartWatcherAsync().TimeoutAfter(DefaultTimeout);
var types = await _app.GetCompiledAppDefinedTypes(); var types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
var folderToDelete = Path.Combine(_app.SourceDirectory, "include"); var folderToDelete = Path.Combine(_app.SourceDirectory, "include");
Directory.Delete(folderToDelete, recursive: true); Directory.Delete(folderToDelete, recursive: true);
await _app.HasRestarted(); await _app.HasRestarted().TimeoutAfter(DefaultTimeout);
types = await _app.GetCompiledAppDefinedTypes(); types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(1, types); Assert.Equal(1, types);
} }
@ -80,19 +83,19 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/23360", Queues = "Debian.9.Arm64;Debian.9.Arm64.Open;(Debian.9.Arm64.Open)Ubuntu.1804.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036;(Debian.9.Arm64)Ubuntu.1804.Armarch@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036")] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/23360", Queues = "Debian.9.Arm64;Debian.9.Arm64.Open;(Debian.9.Arm64.Open)Ubuntu.1804.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036;(Debian.9.Arm64)Ubuntu.1804.Armarch@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036")]
public async Task RenameCompiledFile() public async Task RenameCompiledFile()
{ {
await _app.StartWatcherAsync(); await _app.StartWatcherAsync().TimeoutAfter(DefaultTimeout);
var oldFile = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); var oldFile = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
var newFile = Path.Combine(_app.SourceDirectory, "include", "Foo_new.cs"); var newFile = Path.Combine(_app.SourceDirectory, "include", "Foo_new.cs");
File.Move(oldFile, newFile); File.Move(oldFile, newFile);
await _app.HasRestarted(); await _app.HasRestarted().TimeoutAfter(DefaultTimeout);
} }
[Fact] [Fact]
public async Task ChangeExcludedFile() public async Task ChangeExcludedFile()
{ {
await _app.StartWatcherAsync(); await _app.StartWatcherAsync().TimeoutAfter(DefaultTimeout);
var changedFile = Path.Combine(_app.SourceDirectory, "exclude", "Baz.cs"); var changedFile = Path.Combine(_app.SourceDirectory, "exclude", "Baz.cs");
File.WriteAllText(changedFile, ""); File.WriteAllText(changedFile, "");
@ -105,11 +108,11 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact] [Fact]
public async Task ListsFiles() public async Task ListsFiles()
{ {
await _app.PrepareAsync(); await _app.PrepareAsync().TimeoutAfter(DefaultTimeout);
_app.Start(new[] { "--list" }); _app.Start(new[] { "--list" });
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(30)); cts.CancelAfter(TimeSpan.FromSeconds(30));
var lines = await _app.Process.GetAllOutputLinesAsync(cts.Token); var lines = await _app.Process.GetAllOutputLinesAsync(cts.Token).TimeoutAfter(DefaultTimeout);
var files = lines.Where(l => !l.StartsWith("watch :")); var files = lines.Where(l => !l.StartsWith("watch :"));
AssertEx.EqualFileList( AssertEx.EqualFileList(