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
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
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -7507,6 +7593,13 @@ Global
{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {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}
{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
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

View File

@ -133,10 +133,14 @@ docker run \
-t \
-e TF_BUILD \
-e BUILD_NUMBER \
-e BUILD_BUILDID \
-e SYSTEM_TEAMPROJECT \
-e BUILD_BUILDNUMBER \
-e BUILD_REPOSITORY_URI \
-e BUILD_SOURCEVERSION \
-e BUILD_SOURCEBRANCH \
-e SYSTEM_DEFINITIONID \
-e SYSTEM_TEAMFOUNDATIONCOLLECTIONURI \
-e DOTNET_CLI_TELEMETRY_OPTOUT \
-e Configuration \
-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.
## 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
### 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)
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>
* 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:
* OpenJDK <https://jdk.java.net/>
* 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/>
* 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>
## 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
> 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.
> 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
environment variables.

View File

@ -13,33 +13,33 @@
<Uri>https://github.com/dotnet/blazor</Uri>
<Sha>cc449601d638ffaab58ae9487f0fd010bb178a12</Sha>
</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>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha>
<Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</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>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha>
<Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</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>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha>
<Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</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>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha>
<Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</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>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha>
<Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</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>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha>
<Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</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>
<Sha>58abc390e0e3eb849b5773da3f5ed2982ade521d</Sha>
<Sha>ca2a793016c6980c943325c214f42602910d9991</Sha>
</Dependency>
<Dependency Name="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0-preview.8.20361.2">
<Uri>https://github.com/dotnet/runtime</Uri>

View File

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

View File

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

View File

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

View File

@ -61,6 +61,13 @@ namespace Microsoft.AspNetCore.Components
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()
{
return RequiresExplicitReceiver ? (object)this : Delegate;

View File

@ -56,6 +56,13 @@ namespace Microsoft.AspNetCore.Components
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()
{
return new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate);

View File

@ -498,6 +498,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
{
ProcessRenderQueue();
}
ComponentsProfiling.Instance.End();
}
@ -634,11 +635,43 @@ namespace Microsoft.AspNetCore.Components.RenderTree
var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue();
var disposeComponentState = GetRequiredComponentState(disposeComponentId);
Log.DisposingComponent(_logger, disposeComponentState);
if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var exception))
if (!(disposeComponentState.Component is IAsyncDisposable))
{
exceptions ??= new List<Exception>();
exceptions.Add(exception);
if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var 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);
_batchBuilder.DisposedComponentIds.Append(disposeComponentId);
}

View File

@ -101,6 +101,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
exception = ex;
}
CleanupComponentStateResources(batchBuilder);
return exception == null;
}
private void CleanupComponentStateResources(RenderBatchBuilder batchBuilder)
{
// We don't expect these things to throw.
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames());
@ -110,8 +117,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
DisposeBuffers();
return exception == null;
}
// Callers expect this method to always return a faulted task.
@ -222,5 +227,31 @@ namespace Microsoft.AspNetCore.Components.Rendering
((IDisposable)CurrentRenderTree).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)
{
// 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.
if (string.IsNullOrEmpty(pathSegment))
{

View File

@ -29,10 +29,20 @@ namespace Microsoft.AspNetCore.Components.Routing
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
// and the template do not match, then there is no chance of this matching and
// 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;
}
@ -43,7 +53,15 @@ namespace Microsoft.AspNetCore.Components.Routing
for (var i = 0; i < Template.Segments.Length; 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
// we may need to break out of this for-loop. This can happen
// 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
// also knows which other parameters should be supplied with null values. These
// 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);
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
// that all non-optional segments have matched as well.
var allNonOptionalSegmentsMatch = numMatchingSegments >= (Template.Segments.Length - Template.OptionalSegmentsCount);
if (allRouteSegmentsMatch && allNonOptionalSegmentsMatch)
if (Template.ContainsCatchAllSegment || (allRouteSegmentsMatch && allNonOptionalSegmentsMatch))
{
context.Parameters = parameters;
context.Handler = Handler;

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Components.Routing
TemplateText = templateText;
Segments = segments;
OptionalSegmentsCount = segments.Count(template => template.IsOptional);
ContainsCatchAllSegment = segments.Any(template => template.IsCatchAll);
}
public string TemplateText { get; }
@ -22,5 +23,7 @@ namespace Microsoft.AspNetCore.Components.Routing
public TemplateSegment[] Segments { 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
// simple parameters from it.
// Some differences with ASP.NET Core routes are:
// * We don't support catch all parameter segments.
// * We don't support complex segments.
// The things that we support are:
// * Literal path segments. (Like /Path/To/Some/Page)
// * Parameter path segments (Like /Customer/{Id}/Orders/{OrderId})
// * Catch-all parameters (Like /blog/{*slug})
internal class TemplateParser
{
public static readonly char[] InvalidParameterNameCharacters =
new char[] { '*', '{', '}', '=', '.' };
new char[] { '{', '}', '=', '.' };
internal static RouteTemplate ParseTemplate(string template)
{
@ -80,6 +80,12 @@ namespace Microsoft.AspNetCore.Components.Routing
for (int i = 0; i < templateSegments.Length; 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)
{
continue;

View File

@ -12,34 +12,48 @@ namespace Microsoft.AspNetCore.Components.Routing
{
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
// 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
// a parameter with no type constraints but optionality set
// via the '?' token.
if (segment.IndexOf('?') == segment.Length - 1)
if (Value.IndexOf('?') == Value.Length - 1)
{
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,
// 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.");
}
else
{
Value = segment;
}
Constraints = Array.Empty<RouteConstraint>();
}
else
{
var tokens = segment.Split(':');
var tokens = Value.Split(':');
if (tokens[0].Length == 0)
{
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))
.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.
@ -64,6 +93,8 @@ namespace Microsoft.AspNetCore.Components.Routing
public bool IsOptional { get; }
public bool IsCatchAll { get; }
public RouteConstraint[] Constraints { get; }
public bool Match(string pathSegment, out object? matchedParameterValue)

View File

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

View File

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

View File

@ -487,7 +487,8 @@ namespace Microsoft.AspNetCore.Components.Test
public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer {
var renderer = new TestRenderer
{
ShouldHandleExceptions = true
};
@ -2086,6 +2087,238 @@ namespace Microsoft.AspNetCore.Components.Test
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]
public void RenderBatch_DoesNotDisposeComponentMultipleTimes()
{
@ -2589,7 +2822,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act: Toggle the checkbox
var eventArgs = new ChangeEventArgs { Value = true };
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
var latestBatch = renderer.Batches.Last();
@ -3768,7 +4001,7 @@ namespace Microsoft.AspNetCore.Components.Test
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>();
@ -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
{
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);
}
[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]
public void CanMatchTemplateWithMultipleParameters()
{
@ -247,6 +264,29 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
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[][]
{
new object[] { "/{value:bool}", "/true", true },
@ -476,7 +516,7 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
[Fact]
public void PrefersLiteralTemplateOverParmeterizedTemplates()
public void PrefersLiteralTemplateOverParameterizedTemplates()
{
// Arrange
var routeTable = new TestRouteTableBuilder()

View File

@ -83,6 +83,45 @@ namespace Microsoft.AspNetCore.Components.Routing
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]
public void InvalidTemplate_WithRepeatedParameter()
{
@ -113,7 +152,8 @@ namespace Microsoft.AspNetCore.Components.Routing
}
[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.")]
@ -166,6 +206,26 @@ namespace Microsoft.AspNetCore.Components.Routing
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]
public void InvalidTemplate_BadOptionalCharacterPosition()
{

View File

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

View File

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

View File

@ -71,6 +71,7 @@ namespace Microsoft.AspNetCore.Components.Web
"touch" => Deserialize<TouchEventArgs>(eventArgsJson),
"unknown" => EventArgs.Empty,
"wheel" => Deserialize<WheelEventArgs>(eventArgsJson),
"toggle" => Deserialize<EventArgs>(eventArgsJson),
_ => 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
const bootConfigResult = await BootConfigResult.initAsync();
const bootConfigResult = await BootConfigResult.initAsync(environment);
const [resourceLoader] = await Promise.all([
WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),

View File

@ -2,7 +2,7 @@ export class BootConfigResult {
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', {
method: 'GET',
credentials: 'include',
@ -10,8 +10,8 @@ export class BootConfigResult {
});
// 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.
const applicationEnvironment = bootConfigResponse.headers.get('Blazor-Environment') || 'Production';
// hosts may not. Assume 'Production' in the absence of any specified value.
const applicationEnvironment = environment || bootConfigResponse.headers.get('Blazor-Environment') || 'Production';
const bootConfig: BootJsonData = await bootConfigResponse.json();
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';
interface TimingEntry {

View File

@ -165,6 +165,12 @@ async function getCacheToUseIfEnabled(bootConfig: BootJsonData): Promise<Cache |
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
// 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,

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.
*/
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.

View File

@ -238,7 +238,8 @@ export class BrowserRenderer {
document.createElementNS('http://www.w3.org/2000/svg', tagName) :
document.createElement(tagName);
const newElement = toLogicalElement(newDomElementRaw);
insertLogicalChild(newDomElementRaw, parent, childIndex);
let inserted = false;
// Apply attributes
const descendantsEndIndexExcl = frameIndex + frameReader.subtreeLength(frame);
@ -247,6 +248,8 @@ export class BrowserRenderer {
if (frameReader.frameType(descendantFrame) === FrameType.attribute) {
this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame);
} else {
insertLogicalChild(newDomElementRaw, parent, childIndex);
inserted = true;
// 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
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:
// [1] When inserting a corresponding <option>, in case you're dynamically adding options
// this element did not have any children, so it's not inserted yet.
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
// added as an opaque markup block rather than individually
// Right here we implement [2]
if (newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw) {
// added as an opaque markup block rather than individually. This is the other case below.
// [3] In case the the value of the select and the option value is changed in the same batch.
// 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];
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) {
const containerElement = createAndInsertLogicalContainer(parent, childIndex);
@ -385,13 +411,10 @@ export class BrowserRenderer {
} else {
element.removeAttribute('value');
}
// 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>
const selectElem = this.findClosestAncestorSelectElement(element);
if (selectElem && (selectValuePropname in selectElem) && selectElem[selectValuePropname] === value) {
this.tryApplyValueProperty(batch, selectElem, attributeFrame);
delete selectElem[selectValuePropname];
}
// Situation 3
this.trySetSelectValueFromOptionElement(<HTMLOptionElement>element);
return true;
}
default:

View File

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

View File

@ -89,6 +89,9 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'mousewheel':
return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(event as WheelEvent));
case 'toggle':
return new EventForDotNet<UIEventArgs>('toggle', { type: event.type });
default:
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
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 {
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 EditContext? _fixedEditContext;
private EditContext? _editContext;
private bool _hasSetEditContextExplicitly;
/// <summary>
/// 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
/// from the <see cref="EditContext.Model"/> property.
/// </summary>
[Parameter] public EditContext? EditContext { get; set; }
[Parameter]
public EditContext? EditContext
{
get => _editContext;
set
{
_editContext = value;
_hasSetEditContextExplicitly = value != null;
}
}
/// <summary>
/// Specifies the top-level model object for the form. An edit context will
@ -73,11 +83,16 @@ namespace Microsoft.AspNetCore.Components.Forms
/// <inheritdoc />
protected override void OnParametersSet()
{
if ((EditContext == null) == (Model == null))
if (_hasSetEditContextExplicitly && Model != null)
{
throw new InvalidOperationException($"{nameof(EditForm)} requires a {nameof(Model)} " +
$"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
// (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)}.");
}
// 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
if (_fixedEditContext == null || EditContext != null || Model != _fixedEditContext.Model)
if (Model != null && Model != _editContext?.Model)
{
_fixedEditContext = EditContext ?? new EditContext(Model!);
_editContext = new EditContext(Model!);
}
}
/// <inheritdoc />
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,
// optimizing for the common case where _fixedEditContext never changes.
builder.OpenRegion(_fixedEditContext.GetHashCode());
// optimizing for the common case where _editContext never changes.
builder.OpenRegion(_editContext.GetHashCode());
builder.OpenElement(0, "form");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "onsubmit", _handleSubmitDelegate);
builder.OpenComponent<CascadingValue<EditContext>>(3);
builder.AddAttribute(4, "IsFixed", true);
builder.AddAttribute(5, "Value", _fixedEditContext);
builder.AddAttribute(6, "ChildContent", ChildContent?.Invoke(_fixedEditContext));
builder.AddAttribute(5, "Value", _editContext);
builder.AddAttribute(6, "ChildContent", ChildContent?.Invoke(_editContext));
builder.CloseComponent();
builder.CloseElement();
@ -122,26 +137,26 @@ namespace Microsoft.AspNetCore.Components.Forms
private async Task HandleSubmitAsync()
{
Debug.Assert(_fixedEditContext != null);
Debug.Assert(_editContext != null);
if (OnSubmit.HasDelegate)
{
// When using OnSubmit, the developer takes control of the validation lifecycle
await OnSubmit.InvokeAsync(_fixedEditContext);
await OnSubmit.InvokeAsync(_editContext);
}
else
{
// 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)
{
await OnValidSubmit.InvokeAsync(_fixedEditContext);
await OnValidSubmit.InvokeAsync(_editContext);
}
if (!isValid && OnInvalidSubmit.HasDelegate)
{
await OnInvalidSubmit.InvokeAsync(_fixedEditContext);
await OnInvalidSubmit.InvokeAsync(_editContext);
}
}
}

View File

@ -50,6 +50,12 @@ namespace Microsoft.AspNetCore.Components.Forms
/// </summary>
[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>
/// Gets the associated <see cref="Forms.EditContext"/>.
/// </summary>

View File

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

View File

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

View File

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

View File

@ -122,6 +122,8 @@ namespace Microsoft.AspNetCore.Components.Web
[EventHandler("onpointerlockerror", typeof(EventArgs), true, true)]
[EventHandler("onreadystatechange", typeof(EventArgs), true, true)]
[EventHandler("onscroll", typeof(EventArgs), true, true)]
[EventHandler("ontoggle", typeof(EventArgs), true, true)]
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.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit;
@ -35,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Forms
// Arrange
var model = new TestModel();
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model), ValueExpression = () => model.StringProperty };
await RenderAndGetTestInputComponentAsync(rootComponent);
await InputRenderer.RenderAndGetComponent(rootComponent);
// Act/Assert
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) };
// 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);
}
@ -68,7 +65,7 @@ namespace Microsoft.AspNetCore.Components.Forms
};
// Act
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Assert
Assert.Equal("some value", inputComponent.CurrentValue);
@ -87,7 +84,7 @@ namespace Microsoft.AspNetCore.Components.Forms
};
// Act
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Assert
Assert.Same(rootComponent.EditContext, inputComponent.EditContext);
@ -106,7 +103,7 @@ namespace Microsoft.AspNetCore.Components.Forms
};
// Act
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Assert
Assert.Equal(FieldIdentifier.Create(() => model.StringProperty), inputComponent.FieldIdentifier);
@ -123,7 +120,7 @@ namespace Microsoft.AspNetCore.Components.Forms
Value = "initial value",
ValueExpression = () => model.StringProperty
};
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Equal("initial value", inputComponent.CurrentValue);
// Act
@ -146,7 +143,7 @@ namespace Microsoft.AspNetCore.Components.Forms
ValueChanged = val => valueChangedCallLog.Add(val),
ValueExpression = () => model.StringProperty
};
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Empty(valueChangedCallLog);
// Act
@ -169,7 +166,7 @@ namespace Microsoft.AspNetCore.Components.Forms
ValueChanged = val => valueChangedCallLog.Add(val),
ValueExpression = () => model.StringProperty
};
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Empty(valueChangedCallLog);
// Act
@ -190,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.Forms
Value = "initial value",
ValueExpression = () => model.StringProperty
};
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.False(rootComponent.EditContext.IsModified(() => model.StringProperty));
// Act
@ -213,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.Forms
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
// 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
// Act/Assert: Modify the field
@ -251,7 +248,7 @@ namespace Microsoft.AspNetCore.Components.Forms
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
// Act/Assert
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
Assert.Equal("my-class other-class valid", inputComponent.CssClass);
// Act/Assert: Retains custom class when changing field class
@ -270,7 +267,7 @@ namespace Microsoft.AspNetCore.Components.Forms
Value = new DateTime(1915, 3, 2),
ValueExpression = () => model.DateProperty
};
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act/Assert
Assert.Equal("1915/03/02", inputComponent.CurrentValueAsString);
@ -289,7 +286,7 @@ namespace Microsoft.AspNetCore.Components.Forms
ValueExpression = () => model.DateProperty
};
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
var numValidationStateChanges = 0;
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
@ -319,7 +316,7 @@ namespace Microsoft.AspNetCore.Components.Forms
ValueExpression = () => model.DateProperty
};
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
var numValidationStateChanges = 0;
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
@ -470,21 +467,6 @@ namespace Microsoft.AspNetCore.Components.Forms
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
{
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)
=> 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.
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;
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
@ -19,12 +15,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<TestEnum>
var rootComponent = new TestInputHostComponent<TestEnum, TestInputSelect<TestEnum>>
{
EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableEnum
};
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
inputSelectComponent.CurrentValueAsString = "Two";
@ -38,12 +34,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<TestEnum>
var rootComponent = new TestInputHostComponent<TestEnum, TestInputSelect<TestEnum>>
{
EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableEnum
};
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
inputSelectComponent.CurrentValueAsString = "";
@ -57,12 +53,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<TestEnum?>
var rootComponent = new TestInputHostComponent<TestEnum?, TestInputSelect<TestEnum?>>
{
EditContext = new EditContext(model),
ValueExpression = () => model.NullableEnum
};
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
inputSelectComponent.CurrentValueAsString = "Two";
@ -76,12 +72,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<TestEnum?>
var rootComponent = new TestInputHostComponent<TestEnum?, TestInputSelect<TestEnum?>>
{
EditContext = new EditContext(model),
ValueExpression = () => model.NullableEnum
};
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
inputSelectComponent.CurrentValueAsString = "";
@ -96,12 +92,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<Guid>
var rootComponent = new TestInputHostComponent<Guid, TestInputSelect<Guid>>
{
EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableGuid
};
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
var guid = Guid.NewGuid();
@ -117,12 +113,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<Guid?>
var rootComponent = new TestInputHostComponent<Guid?, TestInputSelect<Guid?>>
{
EditContext = new EditContext(model),
ValueExpression = () => model.NullableGuid
};
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
var guid = Guid.NewGuid();
@ -138,12 +134,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<int>
var rootComponent = new TestInputHostComponent<int, TestInputSelect<int>>
{
EditContext = new EditContext(model),
ValueExpression = () => model.NotNullableInt
};
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
inputSelectComponent.CurrentValueAsString = "42";
@ -158,12 +154,12 @@ namespace Microsoft.AspNetCore.Components.Forms
{
// Arrange
var model = new TestModel();
var rootComponent = new TestInputSelectHostComponent<int?>
var rootComponent = new TestInputHostComponent<int?, TestInputSelect<int?>>
{
EditContext = new EditContext(model),
ValueExpression = () => model.NullableInt
};
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
// Act
inputSelectComponent.CurrentValueAsString = "42";
@ -172,19 +168,30 @@ namespace Microsoft.AspNetCore.Components.Forms
Assert.Equal(42, inputSelectComponent.CurrentValue);
}
private static TestInputSelect<TValue> FindInputSelectComponent<TValue>(CapturedBatch batch)
=> batch.ReferenceFrames
.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)
[Fact]
public async Task ValidationErrorUsesDisplayAttributeName()
{
var testRenderer = new TestRenderer();
var componentId = testRenderer.AssignRootComponentId(hostComponent);
await testRenderer.RenderRootComponentAsync(componentId);
return FindInputSelectComponent<TValue>(testRenderer.Batches.Single());
// Arrange
var model = new TestModel();
var rootComponent = new TestInputHostComponent<int, TestInputSelect<int>>
{
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
@ -218,25 +225,13 @@ namespace Microsoft.AspNetCore.Components.Forms
get => base.CurrentValueAsString;
set => base.CurrentValueAsString = value;
}
}
class TestInputSelectHostComponent<TValue> : AutoRenderComponent
{
public EditContext EditContext { get; set; }
public Expression<Func<TValue>> ValueExpression { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
public async Task SetCurrentValueAsStringAsync(string value)
{
builder.OpenComponent<CascadingValue<EditContext>>(0);
builder.AddAttribute(1, "Value", EditContext);
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<TestInputSelect<TValue>>(0);
childBuilder.AddAttribute(0, "ValueExpression", ValueExpression);
childBuilder.CloseComponent();
}));
builder.CloseComponent();
// 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,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);
}
[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]
public void CanUseJSInteropFromOnAfterRenderAsync()
{

View File

@ -216,6 +216,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("Fourth", () => boundValue.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
// https://github.com/dotnet/aspnetcore/issues/17735
target.SelectByText("Empty value");

View File

@ -115,6 +115,24 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
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]
public void PointerDown_CanTrigger()
{
@ -270,6 +288,17 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
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)
{
// 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);
}
[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]
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">
<!-- Show it also works with optgroup -->
<option value=@string.Empty>Empty value</option>
<option value="@variableValue">Variable value</option>
<option value=@SelectableValue.First>First choice</option>
<option value=@SelectableValue.Second>Second choice</option>
<option value=@SelectableValue.Third>Third choice</option>
@ -253,6 +254,7 @@
</select>
<span id="select-box-value">@selectValue</span>
<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>
<h2>Select (markup block)</h2>
@ -383,13 +385,21 @@
DateTime? timeStepTextboxNullableDateTimeValue = null;
bool includeFourthOption = false;
enum SelectableValue { First, Second, Third, Fourth }
enum SelectableValue { First, Second, Third, Fourth, Fifth, Sixth }
SelectableValue? selectValue = SelectableValue.Second;
SelectableValue? selectMarkupValue = SelectableValue.Second;
SelectableValue variableValue = SelectableValue.Fifth;
void AddAndSelectNewSelectOption()
{
includeFourthOption = true;
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.ReliabilityComponent">Server reliability component</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.RouterTest.NavigationManagerComponent">NavigationManager Test</option>
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
@ -82,6 +83,7 @@
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
<option value="BasicTestApp.TouchEventComponent">Touch events</option>
<option value="BasicTestApp.SelectVariantsComponent">Select with component options</option>
<option value="BasicTestApp.ToggleEventComponent">Toggle Event</option>
</select>
<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 -->
<script src="js/jsinteroptests.js"></script>
<script src="js/renderattributestest.js"></script>
<script src="js/webComponentPerformingJsInterop.js"></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 -->
<script src="js/jsinteroptests.js"></script>
<script src="js/renderattributestest.js"></script>
<script src="js/webComponentPerformingJsInterop.js"></script>
<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
/// server may terminate any remaining active connections.</param>
/// <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>

View File

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

View File

@ -111,7 +111,7 @@ namespace Microsoft.AspNetCore.Http
}
/// <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.
/// </summary>
/// <returns></returns>
@ -128,6 +128,11 @@ namespace Microsoft.AspNetCore.Http
return;
}
if (!_started)
{
await StartAsync();
}
_completed = true;
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>
</form>
<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>
</div>
</div>

View File

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

View File

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

View File

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

View File

@ -71,6 +71,7 @@ namespace Templates.Test
[Theory]
[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)
{
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]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23993")]
public async Task MvcTemplate_RazorRuntimeCompilation_BuildsAndPublishes()
{
Project = await ProjectFactory.GetOrCreateProject("mvc_rc", Output);

View File

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

View File

@ -109,7 +109,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
[Theory]
[MemberData(nameof(LargeUploadData))]
[QuarantinedTest]
[QuarantinedTest] // This is inherently flaky and should never be unquarantined.
public async Task LargeUpload(long? maxRequestBufferSize, bool connectionAdapter, bool expectPause)
{
// 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
// 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.
public static readonly TimeSpan DataExpiration = TimeSpan.FromMinutes(5);
}

View File

@ -49,10 +49,15 @@ namespace Microsoft.AspNetCore.Internal
_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.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;
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);
int peerListenPort = BinaryPrimitives.ReadInt32LittleEndian(buffer);
@ -163,7 +163,7 @@ namespace System.Net.Quic.Implementations.Mock
int bytesRead = 0;
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);
long streamId = BinaryPrimitives.ReadInt64LittleEndian(buffer);

View File

@ -45,7 +45,7 @@ namespace System.Net.Quic.Implementations.Mock
int bytesRead = 0;
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);
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__" }),
binary: "lgGAwKZUYXJnZXSRKpGrX190ZXN0X2lkX18="),
new ProtocolTestData(
name: "InvocationWithMulitpleStreams",
name: "InvocationWithMultipleStreams",
message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }),
binary: "lgGAwKZUYXJnZXSQkqtfX3Rlc3RfaWRfX6xfX3Rlc3RfaWQyX18="),

View File

@ -20,6 +20,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
}
[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")]
public async Task ChangeFileInDependency()
{

View File

@ -15,7 +15,10 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{
public class GlobbingAppTests : IDisposable
{
private GlobbingApp _app;
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);
private readonly GlobbingApp _app;
public GlobbingAppTests(ITestOutputHelper logger)
{
_app = new GlobbingApp(logger);
@ -28,17 +31,17 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
public async Task ChangeCompiledFile(bool 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);
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
var programCs = File.ReadAllText(fileToChange);
File.WriteAllText(fileToChange, programCs);
await _app.HasRestarted();
types = await _app.GetCompiledAppDefinedTypes();
await _app.HasRestarted().TimeoutAfter(DefaultTimeout);
types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
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")]
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);
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
File.Delete(fileToChange);
await _app.HasRestarted();
types = await _app.GetCompiledAppDefinedTypes();
await _app.HasRestarted().TimeoutAfter(DefaultTimeout);
types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
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")]
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);
var folderToDelete = Path.Combine(_app.SourceDirectory, "include");
Directory.Delete(folderToDelete, recursive: true);
await _app.HasRestarted();
types = await _app.GetCompiledAppDefinedTypes();
await _app.HasRestarted().TimeoutAfter(DefaultTimeout);
types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
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")]
public async Task RenameCompiledFile()
{
await _app.StartWatcherAsync();
await _app.StartWatcherAsync().TimeoutAfter(DefaultTimeout);
var oldFile = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
var newFile = Path.Combine(_app.SourceDirectory, "include", "Foo_new.cs");
File.Move(oldFile, newFile);
await _app.HasRestarted();
await _app.HasRestarted().TimeoutAfter(DefaultTimeout);
}
[Fact]
public async Task ChangeExcludedFile()
{
await _app.StartWatcherAsync();
await _app.StartWatcherAsync().TimeoutAfter(DefaultTimeout);
var changedFile = Path.Combine(_app.SourceDirectory, "exclude", "Baz.cs");
File.WriteAllText(changedFile, "");
@ -105,11 +108,11 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact]
public async Task ListsFiles()
{
await _app.PrepareAsync();
await _app.PrepareAsync().TimeoutAfter(DefaultTimeout);
_app.Start(new[] { "--list" });
var cts = new CancellationTokenSource();
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 :"));
AssertEx.EqualFileList(