diff --git a/AspNetCore.sln b/AspNetCore.sln
index 403bbfaa9977..74b293f9b78f 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1726,8 +1726,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestInfrastructure", "TestI
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Mvc.Tests", "src\ProjectTemplates\test\Templates.Mvc.Tests\Templates.Mvc.Tests.csproj", "{AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Blazor.Server.Tests", "src\ProjectTemplates\test\Templates.Blazor.Server.Tests\Templates.Blazor.Server.Tests.csproj", "{281BF9DB-7B8A-446B-9611-10A60903F125}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "stress", "stress", "{A5946454-4788-4871-8F23-A9471D55F115}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.CustomElements", "src\Components\CustomElements\src\Microsoft.AspNetCore.Components.CustomElements.csproj", "{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}"
@@ -10405,22 +10403,6 @@ Global
{AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}.Release|x64.Build.0 = Release|Any CPU
{AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}.Release|x86.ActiveCfg = Release|Any CPU
{AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}.Release|x86.Build.0 = Release|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Debug|arm64.ActiveCfg = Debug|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Debug|arm64.Build.0 = Debug|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Debug|x64.ActiveCfg = Debug|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Debug|x64.Build.0 = Debug|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Debug|x86.ActiveCfg = Debug|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Debug|x86.Build.0 = Debug|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Release|Any CPU.Build.0 = Release|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Release|arm64.ActiveCfg = Release|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Release|arm64.Build.0 = Release|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Release|x64.ActiveCfg = Release|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Release|x64.Build.0 = Release|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Release|x86.ActiveCfg = Release|Any CPU
- {281BF9DB-7B8A-446B-9611-10A60903F125}.Release|x86.Build.0 = Release|Any CPU
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|arm64.ActiveCfg = Debug|Any CPU
@@ -11609,7 +11591,6 @@ Global
{89896261-C5DD-4901-BCA7-7A5F718BC008} = {AB4B9E75-719C-4589-B852-20FBFD727730}
{F0FBA346-D8BC-4FAE-A4B2-85B33FA23055} = {08D53E58-4AAE-40C4-8497-63EC8664F304}
{AA7445F5-BD28-400C-8507-E2E0D3CF7D7E} = {08D53E58-4AAE-40C4-8497-63EC8664F304}
- {281BF9DB-7B8A-446B-9611-10A60903F125} = {08D53E58-4AAE-40C4-8497-63EC8664F304}
{A5946454-4788-4871-8F23-A9471D55F115} = {4FDDC525-4E60-4CAF-83A3-261C5B43721F}
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD} = {0BB58FB6-8B66-4C6D-BA8A-DF3AFAF9AB8F}
{0BB58FB6-8B66-4C6D-BA8A-DF3AFAF9AB8F} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index 0413b558a486..e6807a616d9c 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -62,6 +62,7 @@ and are generated based on the last package release.
+
diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml
index ce0ed56ca4a6..ea5a86530de6 100644
--- a/eng/SourceBuildPrebuiltBaseline.xml
+++ b/eng/SourceBuildPrebuiltBaseline.xml
@@ -6,11 +6,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 76b13dbca737..0765b4f0807b 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -9,329 +9,329 @@
-->
-
+
https://github.com/dotnet/efcore
- ef30eb29656f1924c289c29278192de79ea98b42
+ 26f30ce4f57ff08453662b485af82afadb90a808
-
+
https://github.com/dotnet/efcore
- ef30eb29656f1924c289c29278192de79ea98b42
+ 26f30ce4f57ff08453662b485af82afadb90a808
-
+
https://github.com/dotnet/efcore
- ef30eb29656f1924c289c29278192de79ea98b42
+ 26f30ce4f57ff08453662b485af82afadb90a808
-
+
https://github.com/dotnet/efcore
- ef30eb29656f1924c289c29278192de79ea98b42
+ 26f30ce4f57ff08453662b485af82afadb90a808
-
+
https://github.com/dotnet/efcore
- ef30eb29656f1924c289c29278192de79ea98b42
+ 26f30ce4f57ff08453662b485af82afadb90a808
-
+
https://github.com/dotnet/efcore
- ef30eb29656f1924c289c29278192de79ea98b42
+ 26f30ce4f57ff08453662b485af82afadb90a808
-
+
https://github.com/dotnet/efcore
- ef30eb29656f1924c289c29278192de79ea98b42
+ 26f30ce4f57ff08453662b485af82afadb90a808
-
+
https://github.com/dotnet/efcore
- ef30eb29656f1924c289c29278192de79ea98b42
+ 26f30ce4f57ff08453662b485af82afadb90a808
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/source-build-externals
- 7f9ae67f86a5adc1d9bf2f22f4bf3ec05b6d7b68
+ 06913fc4c3fcb0065ee390d135fb766870d2c38a
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
https://github.com/dotnet/xdt
9a1c3e1b7f0c8763d4c96e593961a61a72679a7b
-
+
https://github.com/dotnet/source-build-reference-packages
- f8ebadcc83f7fc8cfd5147078c87d6e583cb32f1
+ 3e92e7cead1e08476973f637007bb22cde7843ca
@@ -355,47 +355,51 @@
-
+
https://github.com/dotnet/runtime
- 171a525880315369e48c6adf6c181f98357352a5
+ edfe49168f3e6f3d30237a0afeabf5dcae2abad5
-
+
https://github.com/dotnet/arcade
- f3952775e6d00a5b6f43b0615a8a766e095185eb
+ e2334b2be36919347923d0ec872a46acddb1e385
-
+
https://github.com/dotnet/sourcelink
- 23bda65700e70b6697390dcc4e0f87e2dfbce63a
+ 4d2c8bf58e8cb7900ec2d9077c155572e2d3cd88
-
+
https://github.com/dotnet/arcade
- f3952775e6d00a5b6f43b0615a8a766e095185eb
+ e2334b2be36919347923d0ec872a46acddb1e385
-
+
https://github.com/dotnet/arcade
- f3952775e6d00a5b6f43b0615a8a766e095185eb
+ e2334b2be36919347923d0ec872a46acddb1e385
-
+
https://github.com/dotnet/arcade
- f3952775e6d00a5b6f43b0615a8a766e095185eb
+ e2334b2be36919347923d0ec872a46acddb1e385
-
+
https://github.com/dotnet/arcade
- f3952775e6d00a5b6f43b0615a8a766e095185eb
+ e2334b2be36919347923d0ec872a46acddb1e385
-
+
+ https://github.com/dotnet/extensions
+ a0e9c8794e3e0ba27033a9f54a545385228d0876
+
+
https://github.com/nuget/nuget.client
- 027ca8b8ef4b4dc94995f87b9c441d2bcf742c1d
+ 8fef55f5a55a3b4f2c96cd1a9b5ddc51d4b927f8
-
+
https://github.com/nuget/nuget.client
- 027ca8b8ef4b4dc94995f87b9c441d2bcf742c1d
+ 8fef55f5a55a3b4f2c96cd1a9b5ddc51d4b927f8
-
+
https://github.com/nuget/nuget.client
- 027ca8b8ef4b4dc94995f87b9c441d2bcf742c1d
+ 8fef55f5a55a3b4f2c96cd1a9b5ddc51d4b927f8
diff --git a/eng/Versions.props b/eng/Versions.props
index d103a72997da..62a656e3dcdf 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -63,86 +63,88 @@
-->
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
- 8.0.0-preview.6.23314.15
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+ 8.0.0-preview.6.23323.4
+
+ 8.0.0-preview.6.23320.3
- 8.0.0-preview.6.23315.1
- 8.0.0-preview.6.23315.1
- 8.0.0-preview.6.23315.1
- 8.0.0-preview.6.23315.1
- 8.0.0-preview.6.23315.1
- 8.0.0-preview.6.23315.1
- 8.0.0-preview.6.23315.1
- 8.0.0-preview.6.23315.1
+ 8.0.0-preview.6.23323.1
+ 8.0.0-preview.6.23323.1
+ 8.0.0-preview.6.23323.1
+ 8.0.0-preview.6.23323.1
+ 8.0.0-preview.6.23323.1
+ 8.0.0-preview.6.23323.1
+ 8.0.0-preview.6.23323.1
+ 8.0.0-preview.6.23323.1
4.4.0-4.22520.2
4.4.0-4.22520.2
@@ -150,19 +152,19 @@
4.4.0-4.22520.2
- 6.2.2
- 6.2.2
- 6.2.2
+ 6.2.4
+ 6.2.4
+ 6.2.4
- 8.0.0-beta.23312.4
- 8.0.0-beta.23312.4
- 8.0.0-beta.23312.4
+ 8.0.0-beta.23316.6
+ 8.0.0-beta.23316.6
+ 8.0.0-beta.23316.6
- 8.0.0-beta.23309.3
+ 8.0.0-beta.23314.2
- 8.0.0-alpha.1.23305.2
+ 8.0.0-alpha.1.23315.1
- 8.0.0-alpha.1.23314.1
+ 8.0.0-alpha.1.23316.2
7.0.0-preview.22423.2
diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh
index abd045a3247f..7e69e3a9e24a 100755
--- a/eng/common/dotnet-install.sh
+++ b/eng/common/dotnet-install.sh
@@ -54,6 +54,10 @@ cpuname=$(uname -m)
case $cpuname in
arm64|aarch64)
buildarch=arm64
+ if [ "$(getconf LONG_BIT)" -lt 64 ]; then
+ # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS)
+ buildarch=arm
+ fi
;;
loongarch64)
buildarch=loongarch64
diff --git a/global.json b/global.json
index 7089e5870f56..2a4bd537021d 100644
--- a/global.json
+++ b/global.json
@@ -27,7 +27,7 @@
},
"msbuild-sdks": {
"Yarn.MSBuild": "1.22.10",
- "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.23312.4",
- "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.23312.4"
+ "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.23316.6",
+ "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.23316.6"
}
}
diff --git a/src/Caching/Caching.slnf b/src/Caching/Caching.slnf
index ebc2330a777e..dcecdb8a91c7 100644
--- a/src/Caching/Caching.slnf
+++ b/src/Caching/Caching.slnf
@@ -5,7 +5,8 @@
"src\\Caching\\SqlServer\\src\\Microsoft.Extensions.Caching.SqlServer.csproj",
"src\\Caching\\SqlServer\\test\\Microsoft.Extensions.Caching.SqlServer.Tests.csproj",
"src\\Caching\\StackExchangeRedis\\src\\Microsoft.Extensions.Caching.StackExchangeRedis.csproj",
- "src\\Caching\\StackExchangeRedis\\test\\Microsoft.Extensions.Caching.StackExchangeRedis.Tests.csproj"
+ "src\\Caching\\StackExchangeRedis\\test\\Microsoft.Extensions.Caching.StackExchangeRedis.Tests.csproj",
+ "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj"
]
}
-}
+}
\ No newline at end of file
diff --git a/src/Caching/StackExchangeRedis/src/Microsoft.Extensions.Caching.StackExchangeRedis.csproj b/src/Caching/StackExchangeRedis/src/Microsoft.Extensions.Caching.StackExchangeRedis.csproj
index b73694371d1e..b5a3d703a4ab 100644
--- a/src/Caching/StackExchangeRedis/src/Microsoft.Extensions.Caching.StackExchangeRedis.csproj
+++ b/src/Caching/StackExchangeRedis/src/Microsoft.Extensions.Caching.StackExchangeRedis.csproj
@@ -13,14 +13,19 @@
+
+
+
+
+
diff --git a/src/Caching/StackExchangeRedis/src/PublicAPI.Shipped.txt b/src/Caching/StackExchangeRedis/src/PublicAPI/net462/PublicAPI.Shipped.txt
similarity index 100%
rename from src/Caching/StackExchangeRedis/src/PublicAPI.Shipped.txt
rename to src/Caching/StackExchangeRedis/src/PublicAPI/net462/PublicAPI.Shipped.txt
diff --git a/src/Caching/StackExchangeRedis/src/PublicAPI.Unshipped.txt b/src/Caching/StackExchangeRedis/src/PublicAPI/net462/PublicAPI.Unshipped.txt
similarity index 100%
rename from src/Caching/StackExchangeRedis/src/PublicAPI.Unshipped.txt
rename to src/Caching/StackExchangeRedis/src/PublicAPI/net462/PublicAPI.Unshipped.txt
diff --git a/src/Caching/StackExchangeRedis/src/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/Caching/StackExchangeRedis/src/PublicAPI/net8.0/PublicAPI.Shipped.txt
new file mode 100644
index 000000000000..bb997fdc8643
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/src/PublicAPI/net8.0/PublicAPI.Shipped.txt
@@ -0,0 +1,26 @@
+#nullable enable
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Dispose() -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Get(string! key) -> byte[]?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.GetAsync(string! key, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.RedisCache(Microsoft.Extensions.Options.IOptions! optionsAccessor) -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Refresh(string! key) -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.RefreshAsync(string! key, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Remove(string! key) -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.RemoveAsync(string! key, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Set(string! key, byte[]! value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions! options) -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.SetAsync(string! key, byte[]! value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions! options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.Configuration.get -> string?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.Configuration.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ConfigurationOptions.get -> StackExchange.Redis.ConfigurationOptions?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ConfigurationOptions.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ConnectionMultiplexerFactory.get -> System.Func!>?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ConnectionMultiplexerFactory.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.InstanceName.get -> string?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.InstanceName.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ProfilingSession.get -> System.Func?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ProfilingSession.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.RedisCacheOptions() -> void
+Microsoft.Extensions.DependencyInjection.StackExchangeRedisCacheServiceCollectionExtensions
+static Microsoft.Extensions.DependencyInjection.StackExchangeRedisCacheServiceCollectionExtensions.AddStackExchangeRedisCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! setupAction) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Caching/StackExchangeRedis/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Caching/StackExchangeRedis/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000000..a38dbc1bfe7b
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,2 @@
+#nullable enable
+static Microsoft.Extensions.DependencyInjection.StackExchangeRedisCacheServiceCollectionExtensions.AddStackExchangeRedisOutputCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! setupAction) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Caching/StackExchangeRedis/src/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/Caching/StackExchangeRedis/src/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt
new file mode 100644
index 000000000000..bb997fdc8643
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/src/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt
@@ -0,0 +1,26 @@
+#nullable enable
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Dispose() -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Get(string! key) -> byte[]?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.GetAsync(string! key, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.RedisCache(Microsoft.Extensions.Options.IOptions! optionsAccessor) -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Refresh(string! key) -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.RefreshAsync(string! key, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Remove(string! key) -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.RemoveAsync(string! key, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.Set(string! key, byte[]! value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions! options) -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache.SetAsync(string! key, byte[]! value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions! options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.Configuration.get -> string?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.Configuration.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ConfigurationOptions.get -> StackExchange.Redis.ConfigurationOptions?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ConfigurationOptions.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ConnectionMultiplexerFactory.get -> System.Func!>?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ConnectionMultiplexerFactory.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.InstanceName.get -> string?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.InstanceName.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ProfilingSession.get -> System.Func?
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.ProfilingSession.set -> void
+Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions.RedisCacheOptions() -> void
+Microsoft.Extensions.DependencyInjection.StackExchangeRedisCacheServiceCollectionExtensions
+static Microsoft.Extensions.DependencyInjection.StackExchangeRedisCacheServiceCollectionExtensions.AddStackExchangeRedisCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! setupAction) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Caching/StackExchangeRedis/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Caching/StackExchangeRedis/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000000..7dc5c58110bf
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Caching/StackExchangeRedis/src/RedisOutputCacheStore.cs b/src/Caching/StackExchangeRedis/src/RedisOutputCacheStore.cs
new file mode 100644
index 000000000000..5e4905c0cc05
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/src/RedisOutputCacheStore.cs
@@ -0,0 +1,497 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NET7_0_OR_GREATER // IOutputCacheStore only exists from net7
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO.Pipelines;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.OutputCaching;
+using Microsoft.AspNetCore.Shared;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using StackExchange.Redis;
+
+namespace Microsoft.Extensions.Caching.StackExchangeRedis;
+
+internal class RedisOutputCacheStore : IOutputCacheStore, IOutputCacheBufferStore, IDisposable
+{
+ private readonly RedisCacheOptions _options;
+ private readonly ILogger _logger;
+ private readonly RedisKey _valueKeyPrefix;
+ private readonly RedisKey _tagKeyPrefix;
+ private readonly RedisKey _tagMasterKey;
+ private readonly RedisKey[] _tagMasterKeyArray; // for use with Lua if needed (to avoid array allocs)
+ private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
+ private readonly CancellationTokenSource _disposalCancellation = new();
+
+ private bool _disposed;
+ private volatile IDatabase? _cache;
+ private long _lastConnectTicks = DateTimeOffset.UtcNow.Ticks;
+ private long _firstErrorTimeTicks;
+ private long _previousErrorTimeTicks;
+ private bool _useMultiExec, _use62Features;
+
+ internal bool GarbageCollectionEnabled { get; set; } = true;
+
+ // Never reconnect within 60 seconds of the last attempt to connect or reconnect.
+ private readonly TimeSpan ReconnectMinInterval = TimeSpan.FromSeconds(60);
+ // Only reconnect if errors have occurred for at least the last 30 seconds.
+ // This count resets if there are no errors for 30 seconds
+ private readonly TimeSpan ReconnectErrorThreshold = TimeSpan.FromSeconds(30);
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The configuration options.
+ public RedisOutputCacheStore(IOptions optionsAccessor) // TODO: OC-specific options?
+ : this(optionsAccessor, Logging.Abstractions.NullLoggerFactory.Instance.CreateLogger())
+ {
+ }
+
+#if DEBUG
+ internal async ValueTask GetConfigurationInfoAsync(CancellationToken cancellationToken = default)
+ {
+ await ConnectAsync(cancellationToken).ConfigureAwait(false);
+ return $"redis output-cache; MULTI/EXEC: {_useMultiExec}, v6.2+: {_use62Features}";
+ }
+#endif
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The configuration options.
+ /// The logger.
+ internal RedisOutputCacheStore(IOptions optionsAccessor, ILogger logger)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(optionsAccessor);
+ ArgumentNullThrowHelper.ThrowIfNull(logger);
+
+ _options = optionsAccessor.Value;
+ _logger = logger;
+
+ // This allows partitioning a single backend cache for use with multiple apps/services.
+
+ // SE.Redis allows efficient append of key-prefix scenarios, but we can help it
+ // avoid some work/allocations by forcing the key-prefix to be a byte[]; SE.Redis
+ // would do this itself anyway, using UTF8
+ _valueKeyPrefix = (RedisKey)Encoding.UTF8.GetBytes(_options.InstanceName + "__MSOCV_");
+ _tagKeyPrefix = (RedisKey)Encoding.UTF8.GetBytes(_options.InstanceName + "__MSOCT_");
+ _tagMasterKey = (RedisKey)Encoding.UTF8.GetBytes(_options.InstanceName + "__MSOCT");
+ _tagMasterKeyArray = new[] { _tagMasterKey };
+
+ _ = Task.Factory.StartNew(RunGarbageCollectionLoopAsync, default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
+ }
+
+ private async Task RunGarbageCollectionLoopAsync()
+ {
+ try
+ {
+ while (!Volatile.Read(ref _disposed))
+ {
+ // approx every 5 minutes, with some randomization to prevent spikes of pile-on
+ var secondsWithJitter = 300 + Random.Shared.Next(-30, 30);
+ Debug.Assert(secondsWithJitter >= 270 && secondsWithJitter <= 330);
+ await Task.Delay(TimeSpan.FromSeconds(secondsWithJitter)).ConfigureAwait(false);
+ try
+ {
+ if (GarbageCollectionEnabled)
+ {
+ await ExecuteGarbageCollectionAsync(GetExpirationTimestamp(TimeSpan.Zero), _disposalCancellation.Token).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException) when (_disposed)
+ {
+ // fine, service exiting
+ }
+ catch (Exception ex)
+ {
+ // this sweep failed; log it
+ _logger.LogDebug(ex, "Transient error occurred executing redis output-cache GC loop");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // the entire loop is dead
+ _logger.LogDebug(ex, "Fatal error occurred executing redis output-cache GC loop");
+ }
+ }
+
+ internal async ValueTask ExecuteGarbageCollectionAsync(long keepValuesGreaterThan, CancellationToken cancellationToken = default)
+ {
+ var cache = await ConnectAsync(CancellationToken.None).ConfigureAwait(false);
+
+ var gcKey = _tagMasterKey.Append("GC");
+ var gcLifetime = TimeSpan.FromMinutes(5);
+ // value is purely placeholder; it is the existence that matters
+ if (!await cache.StringSetAsync(gcKey, DateTime.UtcNow.ToString(CultureInfo.InvariantCulture), gcLifetime, when: When.NotExists).ConfigureAwait(false))
+ {
+ return null; // competition from another node; not even "nothing"
+ }
+ try
+ {
+ // we'll rely on the enumeration of ZSCAN to spot connection failures, and use "best efforts"
+ // on the individual operations - this avoids per-call latency
+ const CommandFlags GarbageCollectionFlags = CommandFlags.FireAndForget;
+
+ // the score is the effective timeout, so we simply need to cull everything with scores below "cull",
+ // for the individual tag sorted-sets, and also the master sorted-set
+ const int EXTEND_EVERY = 250; // some non-trivial number of work
+ int extendCountdown = EXTEND_EVERY;
+ await foreach (var entry in cache.SortedSetScanAsync(_tagMasterKey).WithCancellation(cancellationToken))
+ {
+ await cache.SortedSetRemoveRangeByScoreAsync(GetTagKey((string)entry.Element!), start: 0, stop: keepValuesGreaterThan, flags: GarbageCollectionFlags).ConfigureAwait(false);
+ if (--extendCountdown <= 0)
+ {
+ await cache.KeyExpireAsync(gcKey, gcLifetime).ConfigureAwait(false);
+ extendCountdown = EXTEND_EVERY;
+ }
+ }
+ // paying latency on the final master-tag purge: is fine
+ return await cache.SortedSetRemoveRangeByScoreAsync(_tagMasterKey, start: 0, stop: keepValuesGreaterThan).ConfigureAwait(false);
+ }
+ finally
+ {
+ await cache.KeyDeleteAsync(gcKey, CommandFlags.FireAndForget).ConfigureAwait(false);
+ }
+ }
+
+ async ValueTask IOutputCacheStore.EvictByTagAsync(string tag, CancellationToken cancellationToken)
+ {
+ var cache = await ConnectAsync(cancellationToken).ConfigureAwait(false);
+ Debug.Assert(cache is not null);
+
+ // we'll use fire-and-forget on individual deletes, relying on the paging mechanism
+ // of ZSCAN to detect fundamental connection problems - so failure will still be reported
+ const CommandFlags DeleteFlags = CommandFlags.FireAndForget;
+
+ var tagKey = GetTagKey(tag);
+ await foreach (var entry in cache.SortedSetScanAsync(tagKey).WithCancellation(cancellationToken))
+ {
+ await cache.KeyDeleteAsync(GetValueKey((string)entry.Element!), DeleteFlags).ConfigureAwait(false);
+ await cache.SortedSetRemoveAsync(tagKey, entry.Element, DeleteFlags).ConfigureAwait(false);
+ }
+ }
+
+ private RedisKey GetValueKey(string key)
+ => _valueKeyPrefix.Append(key);
+
+ private RedisKey GetTagKey(string tag)
+ => _tagKeyPrefix.Append(tag);
+
+ async ValueTask IOutputCacheStore.GetAsync(string key, CancellationToken cancellationToken)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(key);
+
+ var cache = await ConnectAsync(cancellationToken).ConfigureAwait(false);
+ Debug.Assert(cache is not null);
+
+ try
+ {
+ return (byte[]?)(await cache.StringGetAsync(GetValueKey(key)).ConfigureAwait(false));
+ }
+ catch (Exception ex)
+ {
+ OnRedisError(ex, cache);
+ throw;
+ }
+ }
+
+ async ValueTask IOutputCacheBufferStore.TryGetAsync(string key, PipeWriter destination, CancellationToken cancellationToken)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(key);
+ ArgumentNullThrowHelper.ThrowIfNull(destination);
+
+ var cache = await ConnectAsync(cancellationToken).ConfigureAwait(false);
+ Debug.Assert(cache is not null);
+
+ Lease? result = null;
+ try
+ {
+ result = await cache.StringGetLeaseAsync(GetValueKey(key)).ConfigureAwait(false);
+ if (result is null)
+ {
+ return false;
+ }
+
+ // future implementation will pass PipeWriter all the way down through redis,
+ // to allow end-to-end back-pressure; new SE.Redis API required
+ destination.Write(result.Span);
+ await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ result?.Dispose();
+ OnRedisError(ex, cache);
+ throw;
+ }
+ }
+
+ ValueTask IOutputCacheStore.SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ return ((IOutputCacheBufferStore)this).SetAsync(key, new ReadOnlySequence(value), tags.AsMemory(), validFor, cancellationToken);
+ }
+
+ async ValueTask IOutputCacheBufferStore.SetAsync(string key, ReadOnlySequence value, ReadOnlyMemory tags, TimeSpan validFor, CancellationToken cancellationToken)
+ {
+ var cache = await ConnectAsync(cancellationToken).ConfigureAwait(false);
+ Debug.Assert(cache is not null);
+
+ byte[]? leased = null;
+ ReadOnlyMemory singleChunk;
+ if (value.IsSingleSegment)
+ {
+ singleChunk = value.First;
+ }
+ else
+ {
+ int len = checked((int)value.Length);
+ leased = ArrayPool.Shared.Rent(len);
+ value.CopyTo(leased);
+ singleChunk = new(leased, 0, len);
+ }
+
+ await cache.StringSetAsync(GetValueKey(key), singleChunk, validFor).ConfigureAwait(false);
+ // only return lease on success
+ if (leased is not null)
+ {
+ ArrayPool.Shared.Return(leased);
+ }
+
+ if (!tags.IsEmpty)
+ {
+ long expiryTimestamp = GetExpirationTimestamp(validFor);
+ var len = tags.Length;
+
+ // tags are secondary; to avoid latency costs, we'll use fire-and-forget when adding tags - this does
+ // mean that in theory tag-related error may go undetected, but: this is an acceptable trade-off
+ const CommandFlags TagCommandFlags = CommandFlags.FireAndForget;
+
+ for (var i = 0; i < len; i++) // can't use span in async method, so: eat a little overhead here
+ {
+ var tag = tags.Span[i];
+ if (_use62Features)
+ {
+ await cache.SortedSetAddAsync(_tagMasterKey, tag, expiryTimestamp, SortedSetWhen.GreaterThan, TagCommandFlags).ConfigureAwait(false);
+ }
+ else
+ {
+ // semantic equivalent of ZADD GT
+ const string ZADD_GT = """
+ local oldScore = tonumber(redis.call('ZSCORE', KEYS[1], ARGV[2]))
+ if oldScore == nil or oldScore < tonumber(ARGV[1]) then
+ redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2])
+ end
+ """;
+
+ // note we're not sharing an ARGV array between tags here because then we'd need to wait on latency to avoid conflicts;
+ // in reality most caches have very limited tags (if any), so this is not perceived as an issue
+ await cache.ScriptEvaluateAsync(ZADD_GT, _tagMasterKeyArray, new RedisValue[] { expiryTimestamp, tag }, TagCommandFlags).ConfigureAwait(false);
+ }
+ await cache.SortedSetAddAsync(GetTagKey(tag), key, expiryTimestamp, SortedSetWhen.Always, TagCommandFlags).ConfigureAwait(false);
+ }
+ }
+ }
+
+ // note that by necessity we're interleaving two time systems here; the local time, and the
+ // time according to redis (and used internally for redis TTLs); in reality, if we disagree
+ // on time, we have bigger problems, so: this will have to suffice - we cannot reasonably
+ // push our in-proc time into out-of-proc redis
+ // TODO: TimeProvider? ISystemClock?
+ private static long GetExpirationTimestamp(TimeSpan timeout) =>
+ (long)((DateTime.UtcNow + timeout) - DateTime.UnixEpoch).TotalMilliseconds;
+
+ private ValueTask ConnectAsync(CancellationToken token = default)
+ {
+ CheckDisposed();
+ token.ThrowIfCancellationRequested();
+
+ var cache = _cache;
+ if (cache is not null)
+ {
+ return new(cache);
+ }
+ return ConnectSlowAsync(token);
+ }
+ private async ValueTask ConnectSlowAsync(CancellationToken token)
+ {
+ await _connectionLock.WaitAsync(token).ConfigureAwait(false);
+ try
+ {
+ var cache = _cache;
+ if (cache is null)
+ {
+ IConnectionMultiplexer connection;
+ if (_options.ConnectionMultiplexerFactory is null)
+ {
+ if (_options.ConfigurationOptions is not null)
+ {
+ connection = await ConnectionMultiplexer.ConnectAsync(_options.ConfigurationOptions).ConfigureAwait(false);
+ }
+ else
+ {
+ connection = await ConnectionMultiplexer.ConnectAsync(_options.Configuration!).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ connection = await _options.ConnectionMultiplexerFactory().ConfigureAwait(false);
+ }
+
+ PrepareConnection(connection);
+ cache = _cache = connection.GetDatabase();
+ }
+ Debug.Assert(_cache is not null);
+ return cache;
+ }
+ finally
+ {
+ _connectionLock.Release();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+ _disposed = true;
+ _disposalCancellation.Cancel();
+ ReleaseConnection(Interlocked.Exchange(ref _cache, null));
+ }
+
+ private void OnRedisError(Exception exception, IDatabase cache)
+ {
+ if (_options.UseForceReconnect && (exception is RedisConnectionException or SocketException))
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var previousConnectTime = ReadTimeTicks(ref _lastConnectTicks);
+ TimeSpan elapsedSinceLastReconnect = utcNow - previousConnectTime;
+
+ // We want to limit how often we perform this top-level reconnect, so we check how long it's been since our last attempt.
+ if (elapsedSinceLastReconnect < ReconnectMinInterval)
+ {
+ return;
+ }
+
+ var firstErrorTime = ReadTimeTicks(ref _firstErrorTimeTicks);
+ if (firstErrorTime == DateTimeOffset.MinValue)
+ {
+ // note: order/timing here (between the two fields) is not critical
+ WriteTimeTicks(ref _firstErrorTimeTicks, utcNow);
+ WriteTimeTicks(ref _previousErrorTimeTicks, utcNow);
+ return;
+ }
+
+ TimeSpan elapsedSinceFirstError = utcNow - firstErrorTime;
+ TimeSpan elapsedSinceMostRecentError = utcNow - ReadTimeTicks(ref _previousErrorTimeTicks);
+
+ bool shouldReconnect =
+ elapsedSinceFirstError >= ReconnectErrorThreshold // Make sure we gave the multiplexer enough time to reconnect on its own if it could.
+ && elapsedSinceMostRecentError <= ReconnectErrorThreshold; // Make sure we aren't working on stale data (e.g. if there was a gap in errors, don't reconnect yet).
+
+ // Update the previousErrorTime timestamp to be now (e.g. this reconnect request).
+ WriteTimeTicks(ref _previousErrorTimeTicks, utcNow);
+
+ if (!shouldReconnect)
+ {
+ return;
+ }
+
+ WriteTimeTicks(ref _firstErrorTimeTicks, DateTimeOffset.MinValue);
+ WriteTimeTicks(ref _previousErrorTimeTicks, DateTimeOffset.MinValue);
+
+ // wipe the shared field, but *only* if it is still the cache we were
+ // thinking about (once it is null, the next caller will reconnect)
+ ReleaseConnection(Interlocked.CompareExchange(ref _cache, null, cache));
+ }
+ }
+
+ private void PrepareConnection(IConnectionMultiplexer connection)
+ {
+ WriteTimeTicks(ref _lastConnectTicks, DateTimeOffset.UtcNow);
+ ValidateServerFeatures(connection);
+ TryRegisterProfiler(connection);
+ }
+
+ private void ValidateServerFeatures(IConnectionMultiplexer connection)
+ {
+ int serverCount = 0, standaloneCount = 0, v62_Count = 0;
+ foreach (var ep in connection.GetEndPoints())
+ {
+ var server = connection.GetServer(ep);
+ if (server is null)
+ {
+ continue; // wat?
+ }
+ serverCount++;
+ if (server.ServerType == ServerType.Standalone)
+ {
+ standaloneCount++;
+ }
+ if (server.Features.SortedSetRangeStore) // just a random v6.2 feature
+ {
+ v62_Count++;
+ }
+ }
+ _useMultiExec = serverCount == standaloneCount;
+ _use62Features = serverCount == v62_Count;
+ }
+
+ private void TryRegisterProfiler(IConnectionMultiplexer connection)
+ {
+ _ = connection ?? throw new InvalidOperationException($"{nameof(connection)} cannot be null.");
+
+ if (_options.ProfilingSession is not null)
+ {
+ connection.RegisterProfiler(_options.ProfilingSession);
+ }
+ }
+
+ private static void WriteTimeTicks(ref long field, DateTimeOffset value)
+ {
+ var ticks = value == DateTimeOffset.MinValue ? 0L : value.UtcTicks;
+ Volatile.Write(ref field, ticks); // avoid torn values
+ }
+
+ private void CheckDisposed()
+ {
+ ObjectDisposedThrowHelper.ThrowIf(_disposed, this);
+ }
+
+ private static DateTimeOffset ReadTimeTicks(ref long field)
+ {
+ var ticks = Volatile.Read(ref field); // avoid torn values
+ return ticks == 0 ? DateTimeOffset.MinValue : new DateTimeOffset(ticks, TimeSpan.Zero);
+ }
+
+ static void ReleaseConnection(IDatabase? cache)
+ {
+ var connection = cache?.Multiplexer;
+ if (connection is not null)
+ {
+ try
+ {
+ connection.Close();
+ connection.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ }
+ }
+ }
+}
+#endif
diff --git a/src/Caching/StackExchangeRedis/src/RedisOutputCacheStoreImpl.cs b/src/Caching/StackExchangeRedis/src/RedisOutputCacheStoreImpl.cs
new file mode 100644
index 000000000000..e27638aa6400
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/src/RedisOutputCacheStoreImpl.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NET7_0_OR_GREATER // IOutputCacheStore only exists from net7
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Caching.StackExchangeRedis;
+
+internal sealed class RedisOutputCacheStoreImpl : RedisOutputCacheStore
+{
+ public RedisOutputCacheStoreImpl(IOptions optionsAccessor, ILogger logger)
+ : base(optionsAccessor, logger)
+ {
+ }
+
+ public RedisOutputCacheStoreImpl(IOptions optionsAccessor)
+ : base(optionsAccessor)
+ {
+ }
+}
+
+#endif
diff --git a/src/Caching/StackExchangeRedis/src/StackExchangeRedisCacheServiceCollectionExtensions.cs b/src/Caching/StackExchangeRedis/src/StackExchangeRedisCacheServiceCollectionExtensions.cs
index dc9bc9ca5357..d6fca729bf20 100644
--- a/src/Caching/StackExchangeRedis/src/StackExchangeRedisCacheServiceCollectionExtensions.cs
+++ b/src/Caching/StackExchangeRedis/src/StackExchangeRedisCacheServiceCollectionExtensions.cs
@@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Shared;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
+using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection;
@@ -32,4 +33,27 @@ public static IServiceCollection AddStackExchangeRedisCache(this IServiceCollect
return services;
}
+
+#if NET7_0_OR_GREATER
+ ///
+ /// Adds Redis distributed caching services to the specified .
+ ///
+ /// The to add services to.
+ /// An to configure the provided
+ /// .
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddStackExchangeRedisOutputCache(this IServiceCollection services, Action setupAction)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(services);
+ ArgumentNullThrowHelper.ThrowIfNull(setupAction);
+
+ services.AddOptions();
+
+ services.Configure(setupAction);
+ // replace here (Add vs TryAdd) is intentional and part of test conditions
+ services.AddSingleton();
+
+ return services;
+ }
+#endif
}
diff --git a/src/Caching/StackExchangeRedis/test/OutputCache/OutputCacheGetSetTests.cs b/src/Caching/StackExchangeRedis/test/OutputCache/OutputCacheGetSetTests.cs
new file mode 100644
index 000000000000..088d7c76822d
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/test/OutputCache/OutputCacheGetSetTests.cs
@@ -0,0 +1,449 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NET7_0_OR_GREATER
+
+using System;
+using System.Buffers;
+using System.IO.Pipelines;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.OutputCaching;
+using Pipelines.Sockets.Unofficial.Buffers;
+using StackExchange.Redis;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.Caching.StackExchangeRedis;
+
+public class OutputCacheGetSetTests : IClassFixture
+{
+ private const string SkipReason = "TODO: Disabled due to CI failure. " +
+ "These tests require Redis server to be started on the machine. Make sure to change the value of" +
+ "\"RedisTestConfig.RedisPort\" accordingly.";
+
+ private readonly IOutputCacheBufferStore _cache;
+ private readonly RedisConnectionFixture _fixture;
+ private readonly ITestOutputHelper Log;
+
+ public OutputCacheGetSetTests(RedisConnectionFixture connection, ITestOutputHelper log)
+ {
+ _fixture = connection;
+ _cache = new RedisOutputCacheStore(new RedisCacheOptions
+ {
+ ConnectionMultiplexerFactory = () => Task.FromResult(_fixture.Connection),
+ InstanceName = "TestPrefix",
+ })
+ {
+ GarbageCollectionEnabled = false,
+ };
+ Log = log;
+ }
+
+#if DEBUG
+ private async ValueTask Cache()
+ {
+ if (_cache is RedisOutputCacheStore real)
+ {
+ Log.WriteLine(await real.GetConfigurationInfoAsync().ConfigureAwait(false));
+ }
+ return _cache;
+ }
+#else
+ private ValueTask Cache() => new(_cache); // avoid CS1998 - no "await"
+#endif
+
+ [Fact(Skip = SkipReason)]
+ public async Task GetMissingKeyReturnsNull()
+ {
+ var cache = await Cache().ConfigureAwait(false);
+ var result = await cache.GetAsync("non-existent-key", CancellationToken.None);
+ Assert.Null(result);
+ }
+
+ [Theory(Skip = SkipReason)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ public async Task SetStoresValueWithPrefixAndTimeout(bool useReadOnlySequence, bool withTags)
+ {
+ var cache = await Cache().ConfigureAwait(false);
+ var key = Guid.NewGuid().ToString();
+ byte[] storedValue = new byte[1017];
+ Random.Shared.NextBytes(storedValue);
+ RedisKey underlyingKey = "TestPrefix__MSOCV_" + key;
+
+ // pre-check
+ await _fixture.Database.KeyDeleteAsync(new RedisKey[] { "TestPrefix__MSOCT", "TestPrefix__MSOCT_tagA", "TestPrefix__MSOCT_tagB" });
+ var timeout = await _fixture.Database.KeyTimeToLiveAsync(underlyingKey);
+ Assert.Null(timeout); // means doesn't exist
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT"));
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_tagA"));
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_tagB"));
+
+ // act
+ var actTime = DateTime.UtcNow;
+ var ttl = TimeSpan.FromSeconds(30);
+ var tags = withTags ? new[] { "tagA", "tagB" } : null;
+ if (useReadOnlySequence)
+ {
+ await cache.SetAsync(key, new ReadOnlySequence(storedValue), tags, ttl, CancellationToken.None);
+ }
+ else
+ {
+ await cache.SetAsync(key, storedValue, tags, ttl, CancellationToken.None);
+ }
+
+ // validate via redis direct
+ timeout = await _fixture.Database.KeyTimeToLiveAsync(underlyingKey);
+ Assert.NotNull(timeout); // means exists
+ var seconds = timeout.Value.TotalSeconds;
+ Assert.True(seconds >= 28 && seconds <= 32, "timeout should be in range");
+ var redisValue = (byte[])(await _fixture.Database.StringGetAsync(underlyingKey));
+ Assert.True(((ReadOnlySpan)storedValue).SequenceEqual(redisValue), "payload should match");
+
+ double expected = (long)((actTime + ttl) - DateTime.UnixEpoch).TotalMilliseconds;
+ if (withTags)
+ {
+ // we expect the tag structure to now exist, with the scores within a bit of a second
+ const double Tolerance = 100.0;
+ Assert.Equal((await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "tagA")).Value, expected, Tolerance);
+ Assert.Equal((await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "tagB")).Value, expected, Tolerance);
+ Assert.Equal((await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_tagA", key)).Value, expected, Tolerance);
+ Assert.Equal((await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_tagB", key)).Value, expected, Tolerance);
+ }
+ else
+ {
+ // we do *not* expect the tag structure to exist
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT"));
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_tagA"));
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_tagB"));
+ }
+ }
+
+ [Theory(Skip = SkipReason)]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task CanFetchStoredValue(bool useReadOnlySequence)
+ {
+ var cache = await Cache().ConfigureAwait(false);
+ var key = Guid.NewGuid().ToString();
+ byte[] storedValue = new byte[1017];
+ Random.Shared.NextBytes(storedValue);
+
+ // pre-check
+ var fetchedValue = await cache.GetAsync(key, CancellationToken.None);
+ Assert.Null(fetchedValue);
+
+ // store and fetch via service
+ if (useReadOnlySequence)
+ {
+ await cache.SetAsync(key, new ReadOnlySequence(storedValue), null, TimeSpan.FromSeconds(30), CancellationToken.None);
+ }
+ else
+ {
+ await cache.SetAsync(key, storedValue, null, TimeSpan.FromSeconds(30), CancellationToken.None);
+ }
+ fetchedValue = await cache.GetAsync(key, CancellationToken.None);
+ Assert.NotNull(fetchedValue);
+
+ Assert.True(((ReadOnlySpan)storedValue).SequenceEqual(fetchedValue), "payload should match");
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task GetMissingKeyReturnsFalse_BufferWriter() // "true" result checked in MultiSegmentWriteWorksAsExpected_BufferWriter
+ {
+ var cache = Assert.IsAssignableFrom(await Cache().ConfigureAwait(false));
+ var key = Guid.NewGuid().ToString();
+
+ var pipe = new Pipe();
+ Assert.False(await cache.TryGetAsync(key, pipe.Writer, CancellationToken.None));
+ pipe.Writer.Complete();
+ var read = await pipe.Reader.ReadAsync();
+ Assert.True(read.IsCompleted);
+ Assert.True(read.Buffer.IsEmpty);
+ pipe.Reader.AdvanceTo(read.Buffer.End);
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task MasterTagScoreShouldOnlyIncrease()
+ {
+ // store some data
+ var cache = await Cache().ConfigureAwait(false);
+ byte[] storedValue = new byte[1017];
+ Random.Shared.NextBytes(storedValue);
+ var tags = new[] { "gtonly" };
+ await _fixture.Database.KeyDeleteAsync("TestPrefix__MSOCT"); // start from nil state
+
+ await cache.SetAsync(Guid.NewGuid().ToString(), storedValue, tags, TimeSpan.FromSeconds(30), CancellationToken.None);
+ var originalScore = await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "gtonly");
+ Assert.NotNull(originalScore);
+
+ // now store something with a shorter ttl; the score should not change
+ await cache.SetAsync(Guid.NewGuid().ToString(), storedValue, tags, TimeSpan.FromSeconds(15), CancellationToken.None);
+ var newScore = await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "gtonly");
+ Assert.NotNull(newScore);
+ Assert.Equal(originalScore, newScore);
+
+ // now store something with a longer ttl; the score should increase
+ await cache.SetAsync(Guid.NewGuid().ToString(), storedValue, tags, TimeSpan.FromSeconds(45), CancellationToken.None);
+ newScore = await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "gtonly");
+ Assert.NotNull(newScore);
+ Assert.True(newScore > originalScore, "should increase");
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task CanEvictByTag()
+ {
+ // store some data
+ var cache = await Cache().ConfigureAwait(false);
+ byte[] storedValue = new byte[1017];
+ Random.Shared.NextBytes(storedValue);
+ var ttl = TimeSpan.FromSeconds(30);
+
+ var noTags = Guid.NewGuid().ToString();
+ await cache.SetAsync(noTags, storedValue, null, ttl, CancellationToken.None);
+
+ var foo = Guid.NewGuid().ToString();
+ await cache.SetAsync(foo, storedValue, new[] { "foo" }, ttl, CancellationToken.None);
+
+ var bar = Guid.NewGuid().ToString();
+ await cache.SetAsync(bar, storedValue, new[] { "bar" }, ttl, CancellationToken.None);
+
+ var fooBar = Guid.NewGuid().ToString();
+ await cache.SetAsync(fooBar, storedValue, new[] { "foo", "bar" }, ttl, CancellationToken.None);
+
+ // assert prior state
+ Assert.NotNull(await cache.GetAsync(noTags, CancellationToken.None));
+ Assert.NotNull(await cache.GetAsync(foo, CancellationToken.None));
+ Assert.NotNull(await cache.GetAsync(bar, CancellationToken.None));
+ Assert.NotNull(await cache.GetAsync(fooBar, CancellationToken.None));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", noTags));
+ Assert.NotNull(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", foo));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", bar));
+ Assert.NotNull(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", fooBar));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", noTags));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", foo));
+ Assert.NotNull(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", bar));
+ Assert.NotNull(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", fooBar));
+
+ // act
+ for (int i = 0; i < 2; i++) // loop is to ensure no oddity when working on tags that *don't* have entries
+ {
+ await cache.EvictByTagAsync("foo", CancellationToken.None);
+ Assert.NotNull(await cache.GetAsync(noTags, CancellationToken.None));
+ Assert.Null(await cache.GetAsync(foo, CancellationToken.None));
+ Assert.NotNull(await cache.GetAsync(bar, CancellationToken.None));
+ Assert.Null(await cache.GetAsync(fooBar, CancellationToken.None));
+ }
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", noTags));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", foo));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", bar));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", fooBar));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", noTags));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", foo));
+ Assert.NotNull(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", bar));
+ Assert.NotNull(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", fooBar));
+
+ for (int i = 0; i < 2; i++) // loop is to ensure no oddity when working on tags that *don't* have entries
+ {
+ await cache.EvictByTagAsync("bar", CancellationToken.None);
+ Assert.NotNull(await cache.GetAsync(noTags, CancellationToken.None));
+ Assert.Null(await cache.GetAsync(foo, CancellationToken.None));
+ Assert.Null(await cache.GetAsync(bar, CancellationToken.None));
+ Assert.Null(await cache.GetAsync(fooBar, CancellationToken.None));
+ }
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", noTags));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", foo));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", bar));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_foo", fooBar));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", noTags));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", foo));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", bar));
+ Assert.Null(await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT_bar", fooBar));
+
+ // assert expected state
+ Assert.NotNull(await cache.GetAsync(noTags, CancellationToken.None));
+ Assert.Null(await cache.GetAsync(foo, CancellationToken.None));
+ Assert.Null(await cache.GetAsync(bar, CancellationToken.None));
+ Assert.Null(await cache.GetAsync(fooBar, CancellationToken.None));
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task MultiSegmentWriteWorksAsExpected_Array()
+ {
+ // store some data
+ var first = new Segment(1024, null);
+ var second = new Segment(1024, first);
+ var third = new Segment(1024, second);
+
+ Random.Shared.NextBytes(first.Array);
+ Random.Shared.NextBytes(second.Array);
+ Random.Shared.NextBytes(third.Array);
+ var payload = new ReadOnlySequence(first, 800, third, 42);
+ Assert.False(payload.IsSingleSegment, "multi-segment");
+ Assert.Equal(1290, payload.Length); // partial from first and last
+
+ var cache = await Cache().ConfigureAwait(false);
+ var key = Guid.NewGuid().ToString();
+ await cache.SetAsync(key, payload, default, TimeSpan.FromSeconds(30), CancellationToken.None);
+
+ var fetched = await cache.GetAsync(key, CancellationToken.None);
+ Assert.NotNull(fetched);
+ ReadOnlyMemory linear = payload.ToArray();
+ Assert.True(linear.Span.SequenceEqual(fetched), "payload match");
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task MultiSegmentWriteWorksAsExpected_BufferWriter()
+ {
+ // store some data
+ var first = new Segment(1024, null);
+ var second = new Segment(1024, first);
+ var third = new Segment(1024, second);
+
+ Random.Shared.NextBytes(first.Array);
+ Random.Shared.NextBytes(second.Array);
+ Random.Shared.NextBytes(third.Array);
+ var payload = new ReadOnlySequence(first, 800, third, 42);
+ Assert.False(payload.IsSingleSegment, "multi-segment");
+ Assert.Equal(1290, payload.Length); // partial from first and last
+
+ var cache = Assert.IsAssignableFrom(await Cache().ConfigureAwait(false));
+ var key = Guid.NewGuid().ToString();
+ await cache.SetAsync(key, payload, default, TimeSpan.FromSeconds(30), CancellationToken.None);
+
+ var pipe = new Pipe();
+ Assert.True(await cache.TryGetAsync(key, pipe.Writer, CancellationToken.None));
+ pipe.Writer.Complete();
+ var read = await pipe.Reader.ReadAsync();
+ Assert.True(read.IsCompleted);
+ Assert.Equal(1290, read.Buffer.Length);
+
+ using (Linearize(payload, out var linearPayload))
+ using (Linearize(read.Buffer, out var linearRead))
+ {
+ Assert.True(linearPayload.Span.SequenceEqual(linearRead.Span), "payload match");
+ }
+ pipe.Reader.AdvanceTo(read.Buffer.End);
+
+ static IMemoryOwner Linearize(ReadOnlySequence payload, out ReadOnlyMemory linear)
+ {
+ if (payload.IsEmpty)
+ {
+ linear = default;
+ return null;
+ }
+ if (payload.IsSingleSegment)
+ {
+ linear = payload.First;
+ return null;
+ }
+ var len = checked((int)payload.Length);
+ var lease = MemoryPool.Shared.Rent(len);
+ var memory = lease.Memory.Slice(0, len);
+ payload.CopyTo(memory.Span);
+ linear = memory;
+ return lease;
+ }
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task GarbageCollectionDoesNotRunWhenGCKeyHeld()
+ {
+ var cache = await Cache().ConfigureAwait(false);
+ var impl = Assert.IsAssignableFrom(cache);
+ await _fixture.Database.StringSetAsync("TestPrefix__MSOCTGC", "dummy", TimeSpan.FromMinutes(1));
+ try
+ {
+ Assert.Null(await impl.ExecuteGarbageCollectionAsync(42));
+ }
+ finally
+ {
+ await _fixture.Database.KeyDeleteAsync("TestPrefix__MSOCTGC");
+ }
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task GarbageCollectionCleansUpTagData()
+ {
+ // importantly, we're not interested in the lifetime of the *values* - redis deals with that
+ // itself; we're only interested in the tag-expiry metadata
+ var blob = new byte[16];
+ Random.Shared.NextBytes(blob);
+ var cache = await Cache().ConfigureAwait(false);
+ var impl = Assert.IsAssignableFrom(cache);
+
+ // start vanilla
+ await _fixture.Database.KeyDeleteAsync(new RedisKey[] { "TestPrefix__MSOCT",
+ "TestPrefix__MSOCT_a", "TestPrefix__MSOCT_b",
+ "TestPrefix__MSOCT_c", "TestPrefix__MSOCT_d" });
+
+ await cache.SetAsync(Guid.NewGuid().ToString(), blob, new[] { "a", "b" }, TimeSpan.FromSeconds(5), CancellationToken.None); // a=b=5
+ await cache.SetAsync(Guid.NewGuid().ToString(), blob, new[] { "b", "c" }, TimeSpan.FromSeconds(10), CancellationToken.None); // a=5, b=c=10
+ await cache.SetAsync(Guid.NewGuid().ToString(), blob, new[] { "c", "d" }, TimeSpan.FromSeconds(15), CancellationToken.None); // a=5, b=10, c=d=15
+
+ long aScore = (long)await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "a"),
+ bScore = (long)await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "b"),
+ cScore = (long)await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "c"),
+ dScore = (long)await _fixture.Database.SortedSetScoreAsync("TestPrefix__MSOCT", "d");
+
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCTGC"), "GC key should not exist");
+ Assert.True(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT"), "master tag should exist");
+ Assert.True(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_a"), "tag a should exist");
+ Assert.True(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_b"), "tag b should exist");
+ Assert.True(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_c"), "tag c should exist");
+ Assert.True(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_d"), "tag d should exist");
+
+ await CheckCounts(4, 1, 2, 2, 1);
+ Assert.Equal(0, await impl.ExecuteGarbageCollectionAsync(0)); // should not change anything
+ await CheckCounts(4, 1, 2, 2, 1);
+ Assert.Equal(1, await impl.ExecuteGarbageCollectionAsync(aScore)); // 1=removes a
+ await CheckCounts(3, 0, 1, 2, 1);
+ Assert.Equal(1, await impl.ExecuteGarbageCollectionAsync(bScore)); // 1=removes b
+ await CheckCounts(2, 0, 0, 1, 1);
+ Assert.Equal(2, await impl.ExecuteGarbageCollectionAsync(cScore)); // 2=removes c+d
+ await CheckCounts(0, 0, 0, 0, 0);
+ Assert.Equal(0, await impl.ExecuteGarbageCollectionAsync(dScore));
+ await CheckCounts(0, 0, 0, 0, 0);
+ Assert.Equal(0, await impl.ExecuteGarbageCollectionAsync(dScore + 1000)); // should have nothing left to do
+ await CheckCounts(0, 0, 0, 0, 0);
+
+ // we should now not have any of these left
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCTGC"), "GC key should not exist");
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT"), "master tag still exists");
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_a"), "tag a still exists");
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_b"), "tag b still exists");
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_c"), "tag c still exists");
+ Assert.False(await _fixture.Database.KeyExistsAsync("TestPrefix__MSOCT_d"), "tag d still exists");
+
+ async Task CheckCounts(int master, int a, int b, int c, int d)
+ {
+ Assert.Equal(master, (int)await _fixture.Database.SortedSetLengthAsync("TestPrefix__MSOCT"));
+ Assert.Equal(a, (int)await _fixture.Database.SortedSetLengthAsync("TestPrefix__MSOCT_a"));
+ Assert.Equal(b, (int)await _fixture.Database.SortedSetLengthAsync("TestPrefix__MSOCT_b"));
+ Assert.Equal(c, (int)await _fixture.Database.SortedSetLengthAsync("TestPrefix__MSOCT_c"));
+ Assert.Equal(d, (int)await _fixture.Database.SortedSetLengthAsync("TestPrefix__MSOCT_d"));
+ }
+ }
+
+ private class Segment : ReadOnlySequenceSegment
+ {
+ public Segment(int length, Segment previous = null)
+ {
+ if (previous is not null)
+ {
+ previous.Next = this;
+ RunningIndex = previous.RunningIndex + previous.Memory.Length;
+ }
+ Array = new byte[length];
+ Memory = Array;
+ }
+ public byte[] Array { get; }
+ }
+}
+
+#endif
diff --git a/src/Caching/StackExchangeRedis/test/OutputCache/OutputCacheServiceExtensionsTests.cs b/src/Caching/StackExchangeRedis/test/OutputCache/OutputCacheServiceExtensionsTests.cs
new file mode 100644
index 000000000000..37efde7c19da
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/test/OutputCache/OutputCacheServiceExtensionsTests.cs
@@ -0,0 +1,129 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NET7_0_OR_GREATER
+
+using System.Linq;
+using Microsoft.AspNetCore.OutputCaching;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Caching.StackExchangeRedis;
+
+public class OutputCacheServiceExtensionsTests
+{
+ [Fact]
+ public void AddStackExchangeRedisOutputCache_RegistersOutputCacheStoreAsSingleton()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddStackExchangeRedisOutputCache(options => { });
+
+ // Assert
+ var outputCacheStore = services.FirstOrDefault(desc => desc.ServiceType == typeof(IOutputCacheStore));
+
+ Assert.NotNull(outputCacheStore);
+ Assert.Equal(ServiceLifetime.Singleton, outputCacheStore.Lifetime);
+ }
+
+ [Fact]
+ public void AddStackExchangeRedisOutputCache_ReplacesPreviouslyUserRegisteredServices()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddScoped(typeof(IOutputCacheStore), sp => Mock.Of());
+
+ // Act
+ services.AddStackExchangeRedisOutputCache(options => { });
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider();
+
+ var distributedCache = services.FirstOrDefault(desc => desc.ServiceType == typeof(IOutputCacheStore));
+
+ Assert.NotNull(distributedCache);
+ Assert.Equal(ServiceLifetime.Scoped, distributedCache.Lifetime);
+ Assert.IsAssignableFrom(serviceProvider.GetRequiredService());
+ }
+
+ [Fact]
+ public void AddStackExchangeRedisOutputCache_AllowsChaining()
+ {
+ var services = new ServiceCollection();
+
+ Assert.Same(services, services.AddStackExchangeRedisOutputCache(_ => { }));
+ }
+
+ [Fact]
+ public void AddStackExchangeRedisOutputCache_IOutputCacheStoreWithoutLoggingCanBeResolved()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddStackExchangeRedisOutputCache(options => { });
+
+ // Assert
+ using var serviceProvider = services.BuildServiceProvider();
+ var outputCacheStore = serviceProvider.GetRequiredService();
+
+ Assert.NotNull(outputCacheStore);
+ }
+
+ [Fact]
+ public void AddStackExchangeRedisOutputCache_IOutputCacheStoreWithLoggingCanBeResolved()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddStackExchangeRedisOutputCache(options => { });
+ services.AddLogging();
+
+ // Assert
+ using var serviceProvider = services.BuildServiceProvider();
+ var outputCacheStore = serviceProvider.GetRequiredService();
+
+ Assert.NotNull(outputCacheStore);
+ }
+
+ [Fact]
+ public void AddStackExchangeRedisOutputCache_UsesLoggerFactoryAlreadyRegisteredWithServiceCollection()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddScoped(typeof(IOutputCacheStore), sp => Mock.Of());
+
+ var loggerFactory = new Mock();
+
+ loggerFactory
+ .Setup(lf => lf.CreateLogger(It.IsAny()))
+ .Returns((string name) => NullLoggerFactory.Instance.CreateLogger(name))
+ .Verifiable();
+
+ services.AddScoped(typeof(ILoggerFactory), _ => loggerFactory.Object);
+
+ // Act
+ services.AddLogging();
+ services.AddStackExchangeRedisOutputCache(options => { });
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider();
+
+ var distributedCache = services.FirstOrDefault(desc => desc.ServiceType == typeof(IOutputCacheStore));
+
+ Assert.NotNull(distributedCache);
+ Assert.Equal(ServiceLifetime.Scoped, distributedCache.Lifetime);
+ Assert.IsAssignableFrom(serviceProvider.GetRequiredService());
+
+ loggerFactory.Verify();
+ }
+}
+
+#endif
diff --git a/src/Caching/StackExchangeRedis/test/OutputCache/RedisConnectionFixture.cs b/src/Caching/StackExchangeRedis/test/OutputCache/RedisConnectionFixture.cs
new file mode 100644
index 000000000000..6188beb0d691
--- /dev/null
+++ b/src/Caching/StackExchangeRedis/test/OutputCache/RedisConnectionFixture.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NET7_0_OR_GREATER
+
+using System;
+using StackExchange.Redis;
+using Xunit;
+
+namespace Microsoft.Extensions.Caching.StackExchangeRedis;
+
+public class RedisConnectionFixture : IDisposable
+{
+ private readonly ConnectionMultiplexer _muxer;
+ public RedisConnectionFixture()
+ {
+ _muxer = ConnectionMultiplexer.Connect("127.0.0.1:6379");
+ }
+
+ public IDatabase Database => _muxer.GetDatabase();
+
+ public IConnectionMultiplexer Connection => _muxer;
+
+ public void Dispose() => _muxer.Dispose();
+}
+
+#endif
diff --git a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs
index 05609873187d..14823588baf8 100644
--- a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs
+++ b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs
@@ -34,12 +34,11 @@ public AuthorizeRouteViewTest()
serviceCollection.AddSingleton();
serviceCollection.AddSingleton(_testAuthorizationService);
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
var services = serviceCollection.BuildServiceProvider();
_renderer = new TestRenderer(services);
var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer);
- _authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null);
+ _authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null, null);
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
}
diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs
index 994e8430c3d6..0b1b0e2a5d4e 100644
--- a/src/Components/Components/src/Binding/CascadingModelBinder.cs
+++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using System.Reflection.Metadata;
using Microsoft.AspNetCore.Components.Binding;
using Microsoft.AspNetCore.Components.Rendering;
@@ -11,12 +13,13 @@ namespace Microsoft.AspNetCore.Components;
///
/// Defines the binding context for data bound from external sources.
///
-public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, IDisposable
+public sealed class CascadingModelBinder : IComponent, ICascadingValueSupplier, IDisposable
{
+ private readonly Dictionary _providersByCascadingParameterAttributeType = new();
+
private RenderHandle _handle;
private ModelBindingContext? _bindingContext;
private bool _hasPendingQueuedRender;
- private BindingInfo? _bindingInfo;
///
/// The binding context name.
@@ -40,7 +43,9 @@ public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent,
[Inject] internal NavigationManager Navigation { get; set; } = null!;
- [Inject] internal IFormValueSupplier FormValueSupplier { get; set; } = null!;
+ [Inject] internal IEnumerable ModelBindingProviders { get; set; } = Enumerable.Empty();
+
+ internal ModelBindingContext? BindingContext => _bindingContext;
void IComponent.Attach(RenderHandle renderHandle)
{
@@ -110,24 +115,25 @@ internal void UpdateBindingInformation(string url)
// BindingContextId = <>((<>&)|?)handler=my-handler
var name = ModelBindingContext.Combine(ParentContext, Name);
var bindingId = string.IsNullOrEmpty(name) ? "" : GenerateBindingContextId(name);
+ var bindingContextDidChange =
+ _bindingContext is null ||
+ !string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) ||
+ !string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal);
- var bindingContext = _bindingContext != null &&
- string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) &&
- string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal) ?
- _bindingContext : new ModelBindingContext(name, bindingId, FormValueSupplier.CanConvertSingleValue);
-
- // It doesn't matter that we don't check IsFixed, since the CascadingValue we are setting up will throw if the app changes.
- if (IsFixed && _bindingContext != null && _bindingContext != bindingContext)
+ if (bindingContextDidChange)
{
- // Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized
- // as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations:
- // * Component ParentContext hierarchy changes.
- // * Technically, the component won't be retained in this case and will be destroyed instead.
- // * A parent changes Name.
- throw new InvalidOperationException($"'{nameof(CascadingModelBinder)}' 'Name' can't change after initialized.");
- }
+ if (IsFixed && _bindingContext is not null)
+ {
+ // Throw an exception if either the Name or the BindingContextId changed. Once a CascadingModelBinder has been initialized
+ // as fixed, it can't change it's name nor its BindingContextId. This can happen in several situations:
+ // * Component ParentContext hierarchy changes.
+ // * Technically, the component won't be retained in this case and will be destroyed instead.
+ // * A parent changes Name.
+ throw new InvalidOperationException($"'{nameof(CascadingModelBinder)}' 'Name' can't change after initialized.");
+ }
- _bindingContext = bindingContext;
+ _bindingContext = new ModelBindingContext(name, bindingId, CanBind);
+ }
string GenerateBindingContextId(string name)
{
@@ -135,60 +141,92 @@ string GenerateBindingContextId(string name)
var hashIndex = bindingId.IndexOf('#');
return hashIndex == -1 ? bindingId : new string(bindingId.AsSpan(0, hashIndex));
}
+
+ bool CanBind(Type type)
+ {
+ foreach (var provider in ModelBindingProviders)
+ {
+ if (provider.SupportsParameterType(type))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
- void IDisposable.Dispose()
+ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo)
+ => TryGetProvider(in parameterInfo, out var provider)
+ && provider.CanSupplyValue(_bindingContext, parameterInfo);
+
+ void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
- Navigation.LocationChanged -= HandleLocationChanged;
+ // We expect there to always be a provider at this point, because CanSupplyValue must have returned true.
+ var provider = GetProviderOrThrow(parameterInfo);
+
+ if (!provider.AreValuesFixed)
+ {
+ provider.Subscribe(subscriber);
+ }
}
- bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName)
+ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
- var formName = string.IsNullOrEmpty(valueName) ?
- (_bindingContext?.Name) :
- ModelBindingContext.Combine(_bindingContext, valueName);
+ // We expect there to always be a provider at this point, because CanSupplyValue must have returned true.
+ var provider = GetProviderOrThrow(parameterInfo);
- if (_bindingInfo != null &&
- string.Equals(_bindingInfo.FormName, formName, StringComparison.Ordinal) &&
- _bindingInfo.ValueType.Equals(valueType))
+ if (!provider.AreValuesFixed)
{
- // We already bound the value, but some component might have been destroyed and
- // re-created. If the type and name of the value that we bound are the same,
- // we can provide the value that we bound.
- return true;
+ provider.Unsubscribe(subscriber);
}
+ }
- // Can't supply the value if this context is for a form with a different name.
- if (FormValueSupplier.CanBind(formName!, valueType))
- {
- var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue);
- _bindingInfo = new BindingInfo(formName, valueType, bindingSucceeded, boundValue);
- if (!bindingSucceeded)
- {
- // Report errors
- }
+ object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
+ => TryGetProvider(in parameterInfo, out var provider)
+ ? provider.GetCurrentValue(_bindingContext, parameterInfo)
+ : null;
- return true;
+ private CascadingModelBindingProvider GetProviderOrThrow(in CascadingParameterInfo parameterInfo)
+ {
+ if (!TryGetProvider(parameterInfo, out var provider))
+ {
+ throw new InvalidOperationException($"No model binding provider could be found for parameter '{parameterInfo.PropertyName}'.");
}
- return false;
+ return provider;
}
- void ICascadingValueComponent.Subscribe(ComponentState subscriber)
+ private bool TryGetProvider(in CascadingParameterInfo parameterInfo, [NotNullWhen(true)] out CascadingModelBindingProvider? result)
{
- throw new InvalidOperationException("Form values are always fixed.");
- }
+ var attributeType = parameterInfo.Attribute.GetType();
- void ICascadingValueComponent.Unsubscribe(ComponentState subscriber)
- {
- throw new InvalidOperationException("Form values are always fixed.");
- }
+ if (_providersByCascadingParameterAttributeType.TryGetValue(attributeType, out result))
+ {
+ return result is not null;
+ }
- object? ICascadingValueComponent.CurrentValue => _bindingInfo == null ?
- throw new InvalidOperationException("Tried to access form value before it was bound.") :
- _bindingInfo.BoundValue;
+ // We deliberately cache 'null' results to avoid searching for the same attribute type multiple times.
+ result = FindProviderForAttributeType(attributeType);
+ _providersByCascadingParameterAttributeType[attributeType] = result;
+ return result is not null;
- bool ICascadingValueComponent.CurrentValueIsFixed => true;
+ CascadingModelBindingProvider? FindProviderForAttributeType(Type attributeType)
+ {
+ foreach (var provider in ModelBindingProviders)
+ {
+ if (provider.SupportsCascadingParameterAttributeType(attributeType))
+ {
+ return provider;
+ }
+ }
+
+ return null;
+ }
+ }
- private record BindingInfo(string? FormName, Type ValueType, bool BindingResult, object? BoundValue);
+ void IDisposable.Dispose()
+ {
+ Navigation.LocationChanged -= HandleLocationChanged;
+ }
}
diff --git a/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs b/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs
new file mode 100644
index 000000000000..4b75e496a3ce
--- /dev/null
+++ b/src/Components/Components/src/Binding/CascadingModelBindingProvider.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace Microsoft.AspNetCore.Components.Binding;
+
+///
+/// Provides values that get supplied to cascading parameters with .
+///
+public abstract class CascadingModelBindingProvider
+{
+ ///
+ /// Gets whether values supplied by this instance will not change.
+ ///
+ protected internal abstract bool AreValuesFixed { get; }
+
+ ///
+ /// Determines whether this instance can provide values for parameters annotated with the specified attribute type.
+ ///
+ /// The attribute type.
+ /// true if this instance can provide values for parameters annotated with the specified attribute type, otherwise false.
+ protected internal abstract bool SupportsCascadingParameterAttributeType(Type attributeType);
+
+ ///
+ /// Determines whether this instance can provide values to parameters with the specified type.
+ ///
+ /// The parameter type.
+ /// true if this instance can provide values to parameters with the specified type, otherwise false.
+ protected internal abstract bool SupportsParameterType(Type parameterType);
+
+ ///
+ /// Determines whether this instance can supply a value for the specified parameter.
+ ///
+ /// The current .
+ /// The for the component parameter.
+ /// true if a value can be supplied, otherwise false.
+ protected internal abstract bool CanSupplyValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo);
+
+ ///
+ /// Gets the value for the specified parameter.
+ ///
+ /// The current .
+ /// The for the component parameter.
+ /// The value to supply to the parameter.
+ protected internal abstract object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo);
+
+ ///
+ /// Subscribes to changes in supplied values, if they can change.
+ ///
+ ///
+ /// This method must be implemented if is false.
+ ///
+ /// The for the subscribing component.
+ protected internal virtual void Subscribe(ComponentState subscriber)
+ => throw new InvalidOperationException(
+ $"'{nameof(CascadingModelBindingProvider)}' instances that have non-fixed values must provide an implementation for '{nameof(Subscribe)}'.");
+
+ ///
+ /// Unsubscribes from changes in supplied values, if they can change.
+ ///
+ ///
+ /// This method must be implemented if is false.
+ ///
+ /// The for the unsubscribing component.
+ protected internal virtual void Unsubscribe(ComponentState subscriber)
+ => throw new InvalidOperationException(
+ $"'{nameof(CascadingModelBindingProvider)}' instances that have non-fixed values must provide an implementation for '{nameof(Unsubscribe)}'.");
+}
diff --git a/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs b/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs
new file mode 100644
index 000000000000..e6e9c976016b
--- /dev/null
+++ b/src/Components/Components/src/Binding/CascadingQueryModelBindingProvider.cs
@@ -0,0 +1,138 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Components.Routing;
+
+namespace Microsoft.AspNetCore.Components.Binding;
+
+///
+/// Enables component parameters to be supplied from the query string with .
+///
+public sealed class CascadingQueryModelBindingProvider : CascadingModelBindingProvider, IDisposable
+{
+ private readonly QueryParameterValueSupplier _queryParameterValueSupplier = new();
+ private readonly NavigationManager _navigationManager;
+
+ private HashSet? _subscribers;
+ private bool _isSubscribedToLocationChanges;
+ private bool _queryParametersMightHaveChanged = true;
+
+ ///
+ protected internal override bool AreValuesFixed => false;
+
+ ///
+ /// Constructs a new instance of .
+ ///
+ public CascadingQueryModelBindingProvider(NavigationManager navigationManager)
+ {
+ _navigationManager = navigationManager;
+ }
+
+ ///
+ protected internal override bool SupportsCascadingParameterAttributeType(Type attributeType)
+ => attributeType == typeof(SupplyParameterFromQueryAttribute);
+
+ ///
+ protected internal override bool SupportsParameterType(Type type)
+ => QueryParameterValueSupplier.CanSupplyValueForType(type);
+
+ ///
+ protected internal override bool CanSupplyValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo)
+ // We can always supply a value; it'll just be null if there's no match.
+ => true;
+
+ ///
+ protected internal override object? GetCurrentValue(ModelBindingContext? bindingContext, in CascadingParameterInfo parameterInfo)
+ {
+ if (_queryParametersMightHaveChanged)
+ {
+ _queryParametersMightHaveChanged = false;
+ UpdateQueryParameters();
+ }
+
+ var queryParameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName;
+ return _queryParameterValueSupplier.GetQueryParameterValue(parameterInfo.PropertyType, queryParameterName);
+ }
+
+ ///
+ protected internal override void Subscribe(ComponentState subscriber)
+ {
+ SubscribeToLocationChanges();
+
+ _subscribers ??= new();
+ _subscribers.Add(subscriber);
+ }
+
+ ///
+ protected internal override void Unsubscribe(ComponentState subscriber)
+ {
+ _subscribers!.Remove(subscriber);
+
+ if (_subscribers.Count == 0)
+ {
+ UnsubscribeFromLocationChanges();
+ }
+ }
+
+ private void UpdateQueryParameters()
+ {
+ var query = GetQueryString(_navigationManager.Uri);
+
+ _queryParameterValueSupplier.ReadParametersFromQuery(query);
+
+ static ReadOnlyMemory GetQueryString(string url)
+ {
+ var queryStartPos = url.IndexOf('?');
+
+ if (queryStartPos < 0)
+ {
+ return default;
+ }
+
+ var queryEndPos = url.IndexOf('#', queryStartPos);
+ return url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos));
+ }
+ }
+
+ private void SubscribeToLocationChanges()
+ {
+ if (_isSubscribedToLocationChanges)
+ {
+ return;
+ }
+
+ _isSubscribedToLocationChanges = true;
+ _queryParametersMightHaveChanged = true;
+ _navigationManager.LocationChanged += OnLocationChanged;
+ }
+
+ private void UnsubscribeFromLocationChanges()
+ {
+ if (!_isSubscribedToLocationChanges)
+ {
+ return;
+ }
+
+ _isSubscribedToLocationChanges = false;
+ _navigationManager.LocationChanged -= OnLocationChanged;
+ }
+
+ private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
+ {
+ _queryParametersMightHaveChanged = true;
+
+ if (_subscribers is not null)
+ {
+ foreach (var subscriber in _subscribers)
+ {
+ subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
+ }
+ }
+ }
+
+ void IDisposable.Dispose()
+ {
+ UnsubscribeFromLocationChanges();
+ }
+}
diff --git a/src/Components/Components/src/Binding/IFormValueSupplier.cs b/src/Components/Components/src/Binding/IFormValueSupplier.cs
index b8696a224339..4b5403222409 100644
--- a/src/Components/Components/src/Binding/IFormValueSupplier.cs
+++ b/src/Components/Components/src/Binding/IFormValueSupplier.cs
@@ -6,7 +6,7 @@
namespace Microsoft.AspNetCore.Components.Binding;
///
-/// Binds form data valuesto a model.
+/// Binds form data values to a model.
///
public interface IFormValueSupplier
{
diff --git a/src/Components/Components/src/CascadingParameterAttribute.cs b/src/Components/Components/src/CascadingParameterAttribute.cs
index 70cb5998ff72..bb9be43a5b08 100644
--- a/src/Components/Components/src/CascadingParameterAttribute.cs
+++ b/src/Components/Components/src/CascadingParameterAttribute.cs
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components;
/// supplies values with a compatible type and name.
///
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
-public sealed class CascadingParameterAttribute : Attribute
+public sealed class CascadingParameterAttribute : CascadingParameterAttributeBase
{
///
/// If specified, the parameter value will be supplied by the closest
@@ -20,5 +20,5 @@ public sealed class CascadingParameterAttribute : Attribute
/// that supplies a value with a compatible
/// type.
///
- public string? Name { get; set; }
+ public override string? Name { get; set; }
}
diff --git a/src/Components/Components/src/CascadingParameterAttributeBase.cs b/src/Components/Components/src/CascadingParameterAttributeBase.cs
new file mode 100644
index 000000000000..f85fbaa6cffc
--- /dev/null
+++ b/src/Components/Components/src/CascadingParameterAttributeBase.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Components;
+
+///
+/// Represents a parameter whose value cascades down the component hierarchy.
+///
+public abstract class CascadingParameterAttributeBase : Attribute
+{
+ ///
+ /// Gets or sets the name for the parameter, which correlates to the name
+ /// of a cascading value.
+ ///
+ public abstract string? Name { get; set; }
+}
diff --git a/src/Components/Components/src/CascadingParameterInfo.cs b/src/Components/Components/src/CascadingParameterInfo.cs
new file mode 100644
index 000000000000..2d8493ff70f4
--- /dev/null
+++ b/src/Components/Components/src/CascadingParameterInfo.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Components;
+
+///
+/// Contains information about a cascading parameter.
+///
+public readonly struct CascadingParameterInfo
+{
+ ///
+ /// Gets the property's attribute.
+ ///
+ public CascadingParameterAttributeBase Attribute { get; }
+
+ ///
+ /// Gets the name of the parameter's property.
+ ///
+ public string PropertyName { get; }
+
+ ///
+ /// Gets the type of the parameter's property.
+ ///
+ public Type PropertyType { get; }
+
+ internal CascadingParameterInfo(CascadingParameterAttributeBase attribute, string propertyName, Type propertyType)
+ {
+ Attribute = attribute;
+ PropertyName = propertyName;
+ PropertyType = propertyType;
+ }
+}
diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs
index 7af4dc1b9cce..da3ba05c5dc4 100644
--- a/src/Components/Components/src/CascadingParameterState.cs
+++ b/src/Components/Components/src/CascadingParameterState.cs
@@ -13,21 +13,21 @@ namespace Microsoft.AspNetCore.Components;
internal readonly struct CascadingParameterState
{
- private static readonly ConcurrentDictionary _cachedInfos = new();
+ private static readonly ConcurrentDictionary _cachedInfos = new();
- public string LocalValueName { get; }
- public ICascadingValueComponent ValueSupplier { get; }
+ public CascadingParameterInfo ParameterInfo { get; }
+ public ICascadingValueSupplier ValueSupplier { get; }
- public CascadingParameterState(string localValueName, ICascadingValueComponent valueSupplier)
+ public CascadingParameterState(in CascadingParameterInfo parameterInfo, ICascadingValueSupplier valueSupplier)
{
- LocalValueName = localValueName;
+ ParameterInfo = parameterInfo;
ValueSupplier = valueSupplier;
}
public static IReadOnlyList FindCascadingParameters(ComponentState componentState)
{
var componentType = componentState.Component.GetType();
- var infos = GetReflectedCascadingParameterInfos(componentType);
+ var infos = GetCascadingParameterInfos(componentType);
// For components known not to have any cascading parameters, bail out early
if (infos.Length == 0)
@@ -48,90 +48,61 @@ public static IReadOnlyList FindCascadingParameters(Com
{
// Although not all parameters might be matched, we know the maximum number
resultStates ??= new List(infos.Length - infoIndex);
-
- resultStates.Add(new CascadingParameterState(info.ConsumerValueName, supplier));
+ resultStates.Add(new CascadingParameterState(info, supplier));
}
}
return resultStates ?? (IReadOnlyList)Array.Empty();
}
- private static ICascadingValueComponent? GetMatchingCascadingValueSupplier(in ReflectedCascadingParameterInfo info, ComponentState componentState)
+ private static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in CascadingParameterInfo info, ComponentState componentState)
{
var candidate = componentState;
do
{
- if (candidate.Component is ICascadingValueComponent candidateSupplier
- && candidateSupplier.CanSupplyValue(info.ValueType, info.SupplierValueName))
+ if (candidate.Component is ICascadingValueSupplier valueSupplier && valueSupplier.CanSupplyValue(info))
{
- return candidateSupplier;
+ return valueSupplier;
}
- candidate = candidate.ParentComponentState;
+ candidate = candidate.LogicalParentComponentState;
} while (candidate != null);
// No match
return null;
}
- private static ReflectedCascadingParameterInfo[] GetReflectedCascadingParameterInfos(
+ private static CascadingParameterInfo[] GetCascadingParameterInfos(
[DynamicallyAccessedMembers(Component)] Type componentType)
{
if (!_cachedInfos.TryGetValue(componentType, out var infos))
{
- infos = CreateReflectedCascadingParameterInfos(componentType);
+ infos = CreateCascadingParameterInfos(componentType);
_cachedInfos[componentType] = infos;
}
return infos;
}
- private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParameterInfos(
+ private static CascadingParameterInfo[] CreateCascadingParameterInfos(
[DynamicallyAccessedMembers(Component)] Type componentType)
{
- List? result = null;
+ List? result = null;
var candidateProps = ComponentProperties.GetCandidateBindableProperties(componentType);
foreach (var prop in candidateProps)
{
- var attribute = prop.GetCustomAttribute();
- if (attribute != null)
+ var cascadingParameterAttribute = prop.GetCustomAttributes()
+ .OfType().SingleOrDefault();
+ if (cascadingParameterAttribute != null)
{
- result ??= new List();
-
- result.Add(new ReflectedCascadingParameterInfo(
- prop.Name,
- prop.PropertyType,
- attribute.Name));
- }
-
- var hostParameterAttribute = prop.GetCustomAttributes()
- .OfType().SingleOrDefault();
- if (hostParameterAttribute != null)
- {
- result ??= new List();
-
- result.Add(new ReflectedCascadingParameterInfo(
+ result ??= new List();
+ result.Add(new CascadingParameterInfo(
+ cascadingParameterAttribute,
prop.Name,
- prop.PropertyType,
- hostParameterAttribute.Name));
+ prop.PropertyType));
}
}
- return result?.ToArray() ?? Array.Empty();
- }
-
- readonly struct ReflectedCascadingParameterInfo
- {
- public string ConsumerValueName { get; }
- public string? SupplierValueName { get; }
- public Type ValueType { get; }
-
- public ReflectedCascadingParameterInfo(
- string consumerValueName, Type valueType, string? supplierValueName)
- {
- ConsumerValueName = consumerValueName;
- SupplierValueName = supplierValueName;
- ValueType = valueType;
- }
+ return result?.ToArray() ?? Array.Empty();
}
}
diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs
index facb9821e415..a040894ac57c 100644
--- a/src/Components/Components/src/CascadingValue.cs
+++ b/src/Components/Components/src/CascadingValue.cs
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components;
///
/// A component that provides a cascading value to all descendant components.
///
-public class CascadingValue : ICascadingValueComponent, IComponent
+public class CascadingValue : ICascadingValueSupplier, IComponent
{
private RenderHandle _renderHandle;
private HashSet? _subscribers; // Lazily instantiated
@@ -41,10 +41,6 @@ public class CascadingValue : ICascadingValueComponent, IComponent
///
[Parameter] public bool IsFixed { get; set; }
- object? ICascadingValueComponent.CurrentValue => Value;
-
- bool ICascadingValueComponent.CurrentValueIsFixed => IsFixed;
-
///
public void Attach(RenderHandle renderHandle)
{
@@ -130,37 +126,39 @@ public Task SetParametersAsync(ParameterView parameters)
return Task.CompletedTask;
}
- bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string? requestedName)
+ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo)
{
- if (!requestedType.IsAssignableFrom(typeof(TValue)))
+ if (parameterInfo.Attribute is not CascadingParameterAttribute cascadingParameterAttribute || !parameterInfo.PropertyType.IsAssignableFrom(typeof(TValue)))
{
return false;
}
+ // We only consider explicitly requested names, not the property name.
+ var requestedName = cascadingParameterAttribute.Name;
+
return (requestedName == null && Name == null) // Match on type alone
|| string.Equals(requestedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name
}
- void ICascadingValueComponent.Subscribe(ComponentState subscriber)
+ object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
+ {
+ return Value;
+ }
+
+ void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
-#if DEBUG
if (IsFixed)
{
// Should not be possible. User code cannot trigger this.
// Checking only to catch possible future framework bugs.
throw new InvalidOperationException($"Cannot subscribe to a {typeof(CascadingValue<>).Name} when {nameof(IsFixed)} is true.");
}
-#endif
-
- if (_subscribers == null)
- {
- _subscribers = new HashSet();
- }
+ _subscribers ??= new HashSet();
_subscribers.Add(subscriber);
}
- void ICascadingValueComponent.Unsubscribe(ComponentState subscriber)
+ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
_subscribers?.Remove(subscriber);
}
diff --git a/src/Components/Components/src/ComponentFactory.cs b/src/Components/Components/src/ComponentFactory.cs
index e71c61d909e9..b515982a8d1d 100644
--- a/src/Components/Components/src/ComponentFactory.cs
+++ b/src/Components/Components/src/ComponentFactory.cs
@@ -45,12 +45,26 @@ private static ComponentTypeInfoCacheEntry GetComponentTypeInfo([DynamicallyAcce
return cacheEntry;
}
- public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId)
+ public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, IComponentRenderMode? callerSpecifiedRenderMode, int? parentComponentId)
{
- var componentTypeInfo = GetComponentTypeInfo(componentType);
- var component = componentTypeInfo.ComponentTypeRenderMode is null
- ? _componentActivator.CreateInstance(componentType)
- : _renderer.ResolveComponentForRenderMode(componentType, parentComponentId, _componentActivator, componentTypeInfo.ComponentTypeRenderMode);
+ var (componentTypeRenderMode, propertyInjector) = GetComponentTypeInfo(componentType);
+ IComponent component;
+
+ if (componentTypeRenderMode is null && callerSpecifiedRenderMode is null)
+ {
+ // Typical case where no rendermode is specified in either location. We don't call ResolveComponentForRenderMode in this case.
+ component = _componentActivator.CreateInstance(componentType);
+ }
+ else
+ {
+ // At least one rendermode is specified. We require that it's exactly one, and use ResolveComponentForRenderMode with it.
+ var effectiveRenderMode = callerSpecifiedRenderMode is null
+ ? componentTypeRenderMode!
+ : componentTypeRenderMode is null
+ ? callerSpecifiedRenderMode
+ : throw new InvalidOperationException($"The component type '{componentType}' has a fixed rendermode of '{componentTypeRenderMode}', so it is not valid to specify any rendermode when using this component.");
+ component = _renderer.ResolveComponentForRenderMode(componentType, parentComponentId, _componentActivator, effectiveRenderMode);
+ }
if (component is null)
{
@@ -61,7 +75,7 @@ public IComponent InstantiateComponent(IServiceProvider serviceProvider, [Dynami
if (component.GetType() == componentType)
{
// Fast, common case: use the cached data we already looked up
- componentTypeInfo.PerformPropertyInjection(serviceProvider, component);
+ propertyInjector(serviceProvider, component);
}
else
{
diff --git a/src/Components/Components/src/ICascadingValueComponent.cs b/src/Components/Components/src/ICascadingValueComponent.cs
deleted file mode 100644
index b18735c86a9e..000000000000
--- a/src/Components/Components/src/ICascadingValueComponent.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.AspNetCore.Components.Rendering;
-
-namespace Microsoft.AspNetCore.Components;
-
-internal interface ICascadingValueComponent
-{
- // This interface exists only so that CascadingParameterState has a way
- // to work with all CascadingValue types regardless of T.
-
- bool CanSupplyValue(Type valueType, string? valueName);
-
- object? CurrentValue { get; }
-
- bool CurrentValueIsFixed { get; }
-
- void Subscribe(ComponentState subscriber);
-
- void Unsubscribe(ComponentState subscriber);
-}
diff --git a/src/Components/Components/src/ICascadingValueSupplier.cs b/src/Components/Components/src/ICascadingValueSupplier.cs
new file mode 100644
index 000000000000..c535d9cfda16
--- /dev/null
+++ b/src/Components/Components/src/ICascadingValueSupplier.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace Microsoft.AspNetCore.Components;
+
+internal interface ICascadingValueSupplier
+{
+ bool IsFixed { get; }
+
+ bool CanSupplyValue(in CascadingParameterInfo parameterInfo);
+
+ object? GetCurrentValue(in CascadingParameterInfo parameterInfo);
+
+ void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo);
+
+ void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo);
+}
diff --git a/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs b/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs
deleted file mode 100644
index 8f407e0cdd5e..000000000000
--- a/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.AspNetCore.Components;
-
-// Marks a cascading parameter that can be offered via an attribute that is not
-// directly defined in the Components assembly. For example [SupplyParameterFromForm].
-internal interface IHostEnvironmentCascadingParameter
-{
- public string? Name { get; set; }
-}
diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs
index 37ba9c2dfbcd..1b3a25d635d1 100644
--- a/src/Components/Components/src/ParameterView.cs
+++ b/src/Components/Components/src/ParameterView.cs
@@ -423,7 +423,8 @@ public bool MoveNext()
_currentIndex = nextIndex;
var state = _cascadingParameters[_currentIndex];
- _current = new ParameterValue(state.LocalValueName, state.ValueSupplier.CurrentValue!, true);
+ var currentValue = state.ValueSupplier.GetCurrentValue(state.ParameterInfo);
+ _current = new ParameterValue(state.ParameterInfo.PropertyName, currentValue!, true);
return true;
}
else
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 625c0e05e595..2ac249f7283c 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -1,5 +1,16 @@
#nullable enable
+abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.AreValuesFixed.get -> bool
+abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.CanSupplyValue(Microsoft.AspNetCore.Components.ModelBindingContext? bindingContext, in Microsoft.AspNetCore.Components.CascadingParameterInfo parameterInfo) -> bool
+abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.GetCurrentValue(Microsoft.AspNetCore.Components.ModelBindingContext? bindingContext, in Microsoft.AspNetCore.Components.CascadingParameterInfo parameterInfo) -> object?
+abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.SupportsCascadingParameterAttributeType(System.Type! attributeType) -> bool
+abstract Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.SupportsParameterType(System.Type! parameterType) -> bool
+abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.get -> string?
+abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.set -> void
abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
+Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider
+Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.CascadingModelBindingProvider() -> void
+Microsoft.AspNetCore.Components.Binding.CascadingQueryModelBindingProvider
+Microsoft.AspNetCore.Components.Binding.CascadingQueryModelBindingProvider.CascadingQueryModelBindingProvider(Microsoft.AspNetCore.Components.NavigationManager! navigationManager) -> void
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanBind(string! formName, System.Type! valueType) -> bool
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanConvertSingleValue(System.Type! type) -> bool
@@ -12,6 +23,13 @@ Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.get -> bool
Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.set -> void
Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string!
Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void
+Microsoft.AspNetCore.Components.CascadingParameterAttributeBase
+Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.CascadingParameterAttributeBase() -> void
+Microsoft.AspNetCore.Components.CascadingParameterInfo
+Microsoft.AspNetCore.Components.CascadingParameterInfo.Attribute.get -> Microsoft.AspNetCore.Components.CascadingParameterAttributeBase!
+Microsoft.AspNetCore.Components.CascadingParameterInfo.CascadingParameterInfo() -> void
+Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyName.get -> string!
+Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyType.get -> System.Type!
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.IComponentRenderMode
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
@@ -23,9 +41,16 @@ Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collectio
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
+Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentState.get -> Microsoft.AspNetCore.Components.Rendering.ComponentState?
+Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParameter(int sequence, string! name, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
+Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(int sequence, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.SetEventHandlerName(string! eventHandlerName) -> void
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary! routeValues) -> void
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary!
+Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
+Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags.HasCallerSpecifiedRenderMode = 1 -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
+Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentFrameFlags.get -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
+Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.ComponentRenderMode = 9 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary! routeValues) -> void
Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary!
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider
@@ -60,14 +85,25 @@ Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParamete
Microsoft.AspNetCore.Components.StreamRenderingAttribute
Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool
Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void
+*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string?
+*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void
+override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string?
+override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void
override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int
override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool
override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int
override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool
+*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string?
+*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void
+override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string?
+override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void
+virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Subscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void
+virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Unsubscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ResolveComponentForRenderMode(System.Type! componentType, int? parentComponentId, Microsoft.AspNetCore.Components.IComponentActivator! componentActivator, Microsoft.AspNetCore.Components.IComponentRenderMode! componentTypeRenderMode) -> Microsoft.AspNetCore.Components.IComponent!
+virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ResolveComponentForRenderMode(System.Type! componentType, int? parentComponentId, Microsoft.AspNetCore.Components.IComponentActivator! componentActivator, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> Microsoft.AspNetCore.Components.IComponent!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ShouldTrackNamedEventHandlers() -> bool
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.TrackNamedEventId(ulong eventHandlerId, int componentId, string! eventHandlerName) -> void
+~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode
diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs
index 9569e8e6a84e..507d5bcd353a 100644
--- a/src/Components/Components/src/Reflection/ComponentProperties.cs
+++ b/src/Components/Components/src/Reflection/ComponentProperties.cs
@@ -170,8 +170,7 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem
if (propertyInfo != null)
{
if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) &&
- !propertyInfo.IsDefined(typeof(CascadingParameterAttribute)) &&
- !propertyInfo.GetCustomAttributes().OfType().Any())
+ !propertyInfo.GetCustomAttributes().OfType().Any())
{
throw new InvalidOperationException(
$"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
@@ -262,8 +261,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType)
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
{
ParameterAttribute? parameterAttribute = null;
- CascadingParameterAttribute? cascadingParameterAttribute = null;
- IHostEnvironmentCascadingParameter? hostEnvironmentCascadingParameter = null;
+ CascadingParameterAttributeBase? cascadingParameterAttribute = null;
var attributes = propertyInfo.GetCustomAttributes();
foreach (var attribute in attributes)
@@ -273,18 +271,15 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType)
case ParameterAttribute parameter:
parameterAttribute = parameter;
break;
- case CascadingParameterAttribute cascadingParameter:
+ case CascadingParameterAttributeBase cascadingParameter:
cascadingParameterAttribute = cascadingParameter;
break;
- case IHostEnvironmentCascadingParameter hostEnvironmentAttribute:
- hostEnvironmentCascadingParameter = hostEnvironmentAttribute;
- break;
default:
break;
}
}
- var isParameter = parameterAttribute != null || cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null;
+ var isParameter = parameterAttribute != null || cascadingParameterAttribute != null;
if (!isParameter)
{
continue;
@@ -299,7 +294,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType)
var propertySetter = new PropertySetter(targetType, propertyInfo)
{
- Cascading = cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null,
+ Cascading = cascadingParameterAttribute != null,
};
if (_underlyingWriters.ContainsKey(propertyName))
diff --git a/src/Components/Components/src/RenderTree/ComponentFrameFlags.cs b/src/Components/Components/src/RenderTree/ComponentFrameFlags.cs
new file mode 100644
index 000000000000..2fb60cde5518
--- /dev/null
+++ b/src/Components/Components/src/RenderTree/ComponentFrameFlags.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Components.RenderTree;
+
+///
+/// Types in the Microsoft.AspNetCore.Components.RenderTree namespace are not recommended for use outside
+/// of the Blazor framework. These types will change in future release.
+///
+[Flags]
+public enum ComponentFrameFlags : byte
+{
+ ///
+ /// Indicates that the caller has specified a render mode.
+ ///
+ HasCallerSpecifiedRenderMode = 1,
+}
diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
index f51431100e37..c310c5856cfd 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
@@ -953,7 +953,7 @@ private static void InitializeNewComponentFrame(ref DiffContext diffContext, int
var frames = diffContext.NewTree;
ref var frame = ref frames[frameIndex];
var parentComponentId = diffContext.ComponentId;
- var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);
+ var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(frames, frameIndex, parentComponentId);
// Set initial parameters
var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs
index 7d768ce394cd..e85764518942 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs
@@ -136,6 +136,7 @@ public struct RenderTreeFrame
// RenderTreeFrameType.Component
// --------------------------------------------------------------------------------
+ [FieldOffset(6)] internal ComponentFrameFlags ComponentFrameFlagsField;
[FieldOffset(8)] internal int ComponentSubtreeLengthField;
[FieldOffset(12)] internal int ComponentIdField;
[FieldOffset(16)]
@@ -144,6 +145,12 @@ public struct RenderTreeFrame
[FieldOffset(24)] internal ComponentState ComponentStateField;
[FieldOffset(32)] internal object ComponentKeyField;
+ ///
+ /// If the property equals
+ /// gets the for the component frame.
+ ///
+ public ComponentFrameFlags ComponentFrameFlags => ComponentFrameFlagsField;
+
///
/// If the property equals
/// gets the number of frames in the subtree for which this frame is the root.
@@ -250,6 +257,31 @@ public struct RenderTreeFrame
///
public string MarkupContent => MarkupContentField;
+ // --------------------------------------------------------------------------------
+ // RenderTreeFrameType.ComponentRenderMode
+ // --------------------------------------------------------------------------------
+
+ [FieldOffset(16)] internal IComponentRenderMode ComponentRenderModeField;
+
+ ///
+ /// If the property equals ,
+ /// gets the specified . Otherwise, the value is undefined.
+ ///
+ public IComponentRenderMode ComponentRenderMode
+ {
+ get
+ {
+ // Normally we don't check the frame type matches, and leave it to the caller to be responsible for only evaluating the correct properties.
+ // However the name "ComponentRenderMode" sounds so much like it would be a field on Component frames that we'll explicitly check to avoid mistakes.
+ if (FrameType != RenderTreeFrameType.ComponentRenderMode)
+ {
+ throw new InvalidOperationException($"The {nameof(ComponentRenderMode)} field only exists on frames of type {nameof(RenderTreeFrameType.ComponentRenderMode)}.");
+ }
+
+ return ComponentRenderModeField;
+ }
+ }
+
// Element constructor
private RenderTreeFrame(int sequence, int elementSubtreeLength, string elementName, object elementKey)
: this()
diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs
index fad0fddc5c8d..17f06c245c7a 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs
@@ -134,4 +134,19 @@ public void AppendRegion(int sequence)
FrameTypeField = RenderTreeFrameType.Region,
};
}
+
+ public void AppendComponentRenderMode(int sequence, IComponentRenderMode renderMode)
+ {
+ if (_itemsInUse == _items.Length)
+ {
+ GrowBuffer(_items.Length * 2);
+ }
+
+ _items[_itemsInUse++] = new RenderTreeFrame
+ {
+ SequenceField = sequence,
+ FrameTypeField = RenderTreeFrameType.ComponentRenderMode,
+ ComponentRenderModeField = renderMode,
+ };
+ }
}
diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs
index 136c41ed3359..08d734bce58d 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs
@@ -58,4 +58,9 @@ public enum RenderTreeFrameType : short
/// Represents a block of markup content.
///
Markup = 8,
+
+ ///
+ /// Represents an instruction to use a specified render mode for the component.
+ ///
+ ComponentRenderMode = 9,
}
diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs
index 187ffb6d8f79..cc9a4ec91f6f 100644
--- a/src/Components/Components/src/RenderTree/Renderer.cs
+++ b/src/Components/Components/src/RenderTree/Renderer.cs
@@ -126,12 +126,14 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
protected ComponentState GetComponentState(int componentId)
=> GetRequiredComponentState(componentId);
+ internal ComponentState GetComponentState(IComponent component)
+ => _componentStateByComponent.GetValueOrDefault(component);
+
private async void RenderRootComponentsOnHotReload()
{
// Before re-rendering the root component, also clear any well-known caches in the framework
ComponentFactory.ClearCache();
ComponentProperties.ClearCache();
- Routing.QueryParameterValueSupplier.ClearCache();
await Dispatcher.InvokeAsync(() =>
{
@@ -162,7 +164,7 @@ await Dispatcher.InvokeAsync(() =>
/// The type of the component to instantiate.
/// The component instance.
protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType)
- => _componentFactory.InstantiateComponent(_serviceProvider, componentType, null);
+ => _componentFactory.InstantiateComponent(_serviceProvider, componentType, null, null);
///
/// Associates the with the , assigning
@@ -473,19 +475,24 @@ public Type GetEventArgsType(ulong eventHandlerId)
: EventArgsTypeCache.GetEventArgsType(methodInfo);
}
- internal ComponentState InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
+ internal ComponentState InstantiateChildComponentOnFrame(RenderTreeFrame[] frames, int frameIndex, int parentComponentId)
{
+ ref var frame = ref frames[frameIndex];
if (frame.FrameTypeField != RenderTreeFrameType.Component)
{
- throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frame));
+ throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frameIndex));
}
if (frame.ComponentStateField != null)
{
- throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame));
+ throw new ArgumentException($"The frame already has a non-null component instance", nameof(frameIndex));
}
- var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, parentComponentId);
+ var callerSpecifiedRenderMode = frame.ComponentFrameFlags.HasFlag(ComponentFrameFlags.HasCallerSpecifiedRenderMode)
+ ? FindCallerSpecifiedRenderMode(frames, frameIndex)
+ : null;
+
+ var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, callerSpecifiedRenderMode, parentComponentId);
var newComponentState = AttachAndInitComponent(newComponent, parentComponentId);
frame.ComponentStateField = newComponentState;
frame.ComponentIdField = newComponentState.ComponentId;
@@ -493,6 +500,30 @@ internal ComponentState InstantiateChildComponentOnFrame(ref RenderTreeFrame fra
return newComponentState;
}
+ private static IComponentRenderMode? FindCallerSpecifiedRenderMode(RenderTreeFrame[] frames, int componentFrameIndex)
+ {
+ // ComponentRenderMode frames are immediate children of Component frames. So, they have to appear after any parameter
+ // attributes (since attributes must always immediately follow Component frames), but before anything that would
+ // represent a different child node, such as text/element or another component. It's OK to do this linear scan
+ // because we consider it uncommon to specify a rendermode, and none of this happens if you don't.
+ var endIndex = componentFrameIndex + frames[componentFrameIndex].ComponentSubtreeLengthField;
+ for (var index = componentFrameIndex + 1; index <= endIndex; index++)
+ {
+ ref var frame = ref frames[index];
+ switch (frame.FrameType)
+ {
+ case RenderTreeFrameType.Attribute:
+ continue;
+ case RenderTreeFrameType.ComponentRenderMode:
+ return frame.ComponentRenderMode;
+ default:
+ break;
+ }
+ }
+
+ return null;
+ }
+
internal void AddToPendingTasksWithErrorHandling(Task task, ComponentState? owningComponentState)
{
switch (task == null ? TaskStatus.RanToCompletion : task.Status)
@@ -1039,7 +1070,7 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er
return; // Handled successfully
}
- candidate = candidate.ParentComponentState;
+ candidate = candidate.LogicalParentComponentState;
}
// It's unhandled, so treat as fatal
@@ -1144,23 +1175,24 @@ void NotifyExceptions(List exceptions)
///
/// Determines how to handle an when obtaining a component instance.
- /// This is only called for components that have specified a render mode. Subclasses may override this
- /// method to return a component of a different type, or throw, depending on whether the renderer
+ /// This is only called when a render mode is specified either at the call site or on the component type.
+ ///
+ /// Subclasses may override this method to return a component of a different type, or throw, depending on whether the renderer
/// supports the render mode and how it implements that support.
///
/// The type of component that was requested.
/// The parent component ID, or null if it is a root component.
/// An that should be used when instantiating component objects.
- /// The declared on .
+ /// The declared on or at the call site (for example, by the parent component).
/// An instance.
protected internal virtual IComponent ResolveComponentForRenderMode(
[DynamicallyAccessedMembers(Component)] Type componentType,
int? parentComponentId,
IComponentActivator componentActivator,
- IComponentRenderMode componentTypeRenderMode)
+ IComponentRenderMode renderMode)
{
// Nothing is supported by default. Subclasses must override this to opt into supporting specific render modes.
- throw new NotSupportedException($"Cannot supply a component of type '{componentType}' because the current platform does not support the render mode '{componentTypeRenderMode}'.");
+ throw new NotSupportedException($"Cannot supply a component of type '{componentType}' because the current platform does not support the render mode '{renderMode}'.");
}
///
diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs
index f7de9f106215..a98adba82b0f 100644
--- a/src/Components/Components/src/Rendering/ComponentState.cs
+++ b/src/Components/Components/src/Rendering/ComponentState.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Components.RenderTree;
+using Microsoft.AspNetCore.Components.Sections;
namespace Microsoft.AspNetCore.Components.Rendering;
@@ -34,6 +35,9 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
ComponentId = componentId;
ParentComponentState = parentComponentState;
Component = component ?? throw new ArgumentNullException(nameof(component));
+ LogicalParentComponentState = component is SectionOutlet.SectionOutletContentRenderer
+ ? (GetSectionOutletLogicalParent(renderer, (SectionOutlet)parentComponentState!.Component) ?? parentComponentState)
+ : parentComponentState;
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_cascadingParameters = CascadingParameterState.FindCascadingParameters(this);
CurrentRenderTree = new RenderTreeBuilder();
@@ -46,6 +50,18 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
}
}
+ private static ComponentState? GetSectionOutletLogicalParent(Renderer renderer, SectionOutlet sectionOutlet)
+ {
+ // This will return null if the SectionOutlet is not currently rendering any content
+ if (sectionOutlet.CurrentLogicalParent is { } logicalParent
+ && renderer.GetComponentState(logicalParent) is { } logicalParentComponentState)
+ {
+ return logicalParentComponentState;
+ }
+
+ return null;
+ }
+
///
/// Gets the component ID.
///
@@ -61,6 +77,11 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
///
public ComponentState? ParentComponentState { get; }
+ ///
+ /// Gets the of the logical parent component, or null if this is a root component.
+ ///
+ public ComponentState? LogicalParentComponentState { get; }
+
internal RenderTreeBuilder CurrentRenderTree { get; set; }
internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, out Exception? renderFragmentException)
@@ -194,9 +215,9 @@ private bool AddCascadingParameterSubscriptions()
for (var i = 0; i < numCascadingParameters; i++)
{
var valueSupplier = _cascadingParameters[i].ValueSupplier;
- if (!valueSupplier.CurrentValueIsFixed)
+ if (!valueSupplier.IsFixed)
{
- valueSupplier.Subscribe(this);
+ valueSupplier.Subscribe(this, _cascadingParameters[i].ParameterInfo);
hasSubscription = true;
}
}
@@ -210,9 +231,9 @@ private void RemoveCascadingParameterSubscriptions()
for (var i = 0; i < numCascadingParameters; i++)
{
var supplier = _cascadingParameters[i].ValueSupplier;
- if (!supplier.CurrentValueIsFixed)
+ if (!supplier.IsFixed)
{
- supplier.Unsubscribe(this);
+ supplier.Unsubscribe(this, _cascadingParameters[i].ParameterInfo);
}
}
}
diff --git a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs
index 874a32a99a39..cca97d250c24 100644
--- a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs
+++ b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs
@@ -28,6 +28,7 @@ public sealed class RenderTreeBuilder : IDisposable
private bool _hasSeenAddMultipleAttributes;
private Dictionary? _seenAttributeNames;
private Dictionary? _seenEventHandlerNames;
+ private IComponentRenderMode? _pendingComponentCallSiteRenderMode; // TODO: Remove when Razor compiler supports call-site @rendermode
// Configure the render tree builder to capture the event handler names.
internal bool TrackNamedEventHandlers { get; set; }
@@ -545,6 +546,28 @@ public void OpenComponent(int sequence, [DynamicallyAccessedMembers(Component)]
OpenComponentUnchecked(sequence, componentType);
}
+ ///
+ /// Temporary API until Razor compiler is updated. This will be removed before .NET 8 ships.
+ ///
+ public void AddComponentParameter(int sequence, string name, IComponentRenderMode renderMode)
+ {
+ if (string.Equals(name, "@rendermode", StringComparison.Ordinal))
+ {
+ // When the Razor compiler is updated, would compile directly as a call
+ // to AddComponentRenderMode(RenderMode.WebAssembly), which must appear after all attributes. Until then we'll intercept regular
+ // parameters with this name and IComponentRenderMode values. Unfortunately we can't guarantee that the parameter will appear after
+ // all other parameters (e.g., ChildContent would always go later), so use this inefficient trick to defer adding it.
+ // It won't be needed once the Razor compiler supports @rendermode.
+ _pendingComponentCallSiteRenderMode = renderMode;
+ }
+ else
+ {
+ // For other parameter names, the developer is doing something custom so just pass the parameter as normal
+ // This special case will also not be relevant once we have @rendermode
+ AddComponentParameter(sequence, name, (object)renderMode);
+ }
+ }
+
///
/// Appends a frame representing a component parameter.
///
@@ -612,6 +635,12 @@ private void OpenComponentUnchecked(int sequence, [DynamicallyAccessedMembers(Co
///
public void CloseComponent()
{
+ if (_pendingComponentCallSiteRenderMode is not null)
+ {
+ AddComponentRenderMode(0, _pendingComponentCallSiteRenderMode);
+ _pendingComponentCallSiteRenderMode = null;
+ }
+
var indexOfEntryBeingClosed = _openElementIndices.Pop();
// We might be closing a component with only attributes. Run the attribute cleanup pass
@@ -663,6 +692,41 @@ public void AddComponentReferenceCapture(int sequence, Action