Merge branch 'master' into merge/release/5.0-preview8-to-master
This commit is contained in:
commit
537ab96eb2
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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" \
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<!--
|
||||
|
||||
|
|
|
|||
|
|
@ -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($@"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ app {
|
|||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.main {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 || {}),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const nonBubblingEvents = toLookup([
|
|||
'scroll',
|
||||
'submit',
|
||||
'unload',
|
||||
'toggle',
|
||||
'DOMNodeInsertedIntoDocument',
|
||||
'DOMNodeRemovedFromDocument',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
@page "/WithCatchAllParameter/{*theAnswer}"
|
||||
<div id="test-info">The answer: @TheAnswer.</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public string TheAnswer { get; set; }
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ app {
|
|||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.main {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ app {
|
|||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.main {
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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="),
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue