diff --git a/.azure/pipelines/components-e2e-tests.yml b/.azure/pipelines/components-e2e-tests.yml
index fc0acb70ba41..2386635fa813 100644
--- a/.azure/pipelines/components-e2e-tests.yml
+++ b/.azure/pipelines/components-e2e-tests.yml
@@ -98,6 +98,8 @@ jobs:
exit 1
fi
displayName: Run E2E tests
+ env:
+ DOTNET_EnableAVX512: 0
- script: .dotnet/dotnet test ./src/Components/test/E2ETest -c $(BuildConfiguration) --no-build --filter 'Quarantined=true' -p:RunQuarantinedTests=true
-p:VsTestUseMSBuildOutput=false
--logger:"trx%3BLogFileName=Microsoft.AspNetCore.Components.E2ETests.trx"
@@ -105,6 +107,8 @@ jobs:
--results-directory $(Build.SourcesDirectory)/artifacts/TestResults/$(BuildConfiguration)/Quarantined
displayName: Run Quarantined E2E tests
continueOnError: true
+ env:
+ DOTNET_EnableAVX512: 0
- task: PublishTestResults@2
displayName: Publish E2E Test Results
inputs:
diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml
index 50d4fd36031d..4a9c0e892785 100644
--- a/.github/policies/resourceManagement.yml
+++ b/.github/policies/resourceManagement.yml
@@ -614,7 +614,7 @@ configuration:
then:
- removeMilestone
- addMilestone:
- milestone: 9.0.7
+ milestone: 9.0.9
description: '[Milestone Assignments] Assign Milestone to PRs merged to release/9.0 branch'
- if:
- payloadType: Pull_Request
@@ -625,7 +625,7 @@ configuration:
then:
- removeMilestone
- addMilestone:
- milestone: 8.0.18
+ milestone: 8.0.20
description: '[Milestone Assignments] Assign Milestone to PRs merged to release/8.0 branch'
- if:
- payloadType: Issues
@@ -742,3 +742,4 @@ configuration:
description: '[Infrastructure PRs] Add area-infrastructure label to dependabot update Pull Requests & enable auto-merge'
onFailure:
onSuccess:
+
diff --git a/eng/Version.Details.props b/eng/Version.Details.props
index aa1fc141196d..0852ee929424 100644
--- a/eng/Version.Details.props
+++ b/eng/Version.Details.props
@@ -1 +1,241 @@
-
+
+
+
+
+
+ 10.0.0-rc.1.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-beta.25377.103
+ 10.0.0-beta.25377.103
+ 10.0.0-beta.25377.103
+ 10.0.0-beta.25377.103
+ 10.0.0-beta.25377.103
+ 10.0.0-beta.25377.103
+ 10.0.0-beta.25377.103
+ 10.0.0-rc.1.25377.103
+ 10.0.0-rc.1.25377.103
+ 10.0.0-rc.1.25377.103
+ 10.0.0-rc.1.25377.103
+ 10.0.0-rc.1.25377.103
+ 10.0.0-rc.1.25377.103
+ 10.0.0-rc.1.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 3.2.0-preview.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+ 10.0.0-preview.7.25377.103
+
+ 4.13.0-3.24613.7
+ 4.13.0-3.24613.7
+ 4.13.0-3.24613.7
+ 4.13.0-3.24613.7
+
+ 9.8.0-preview.1.25401.1
+ 9.8.0-preview.1.25401.1
+ 9.8.0-preview.1.25401.1
+
+ 1.0.0-prerelease.25374.3
+ 1.0.0-prerelease.25374.3
+ 1.0.0-prerelease.25374.3
+ 1.0.0-prerelease.25374.3
+ 1.0.0-prerelease.25374.3
+
+ 17.12.36
+ 17.12.36
+ 17.12.36
+ 17.12.36
+
+ 6.2.4
+ 6.2.4
+ 6.2.4
+
+
+
+
+ $(dotnetefPackageVersion)
+ $(MicrosoftBclAsyncInterfacesPackageVersion)
+ $(MicrosoftBclTimeProviderPackageVersion)
+ $(MicrosoftDotNetArcadeSdkPackageVersion)
+ $(MicrosoftDotNetBuildTasksArchivesPackageVersion)
+ $(MicrosoftDotNetBuildTasksInstallersPackageVersion)
+ $(MicrosoftDotNetBuildTasksTemplatingPackageVersion)
+ $(MicrosoftDotNetHelixSdkPackageVersion)
+ $(MicrosoftDotNetRemoteExecutorPackageVersion)
+ $(MicrosoftDotNetSharedFrameworkSdkPackageVersion)
+ $(MicrosoftEntityFrameworkCorePackageVersion)
+ $(MicrosoftEntityFrameworkCoreDesignPackageVersion)
+ $(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)
+ $(MicrosoftEntityFrameworkCoreRelationalPackageVersion)
+ $(MicrosoftEntityFrameworkCoreSqlitePackageVersion)
+ $(MicrosoftEntityFrameworkCoreSqlServerPackageVersion)
+ $(MicrosoftEntityFrameworkCoreToolsPackageVersion)
+ $(MicrosoftExtensionsCachingAbstractionsPackageVersion)
+ $(MicrosoftExtensionsCachingMemoryPackageVersion)
+ $(MicrosoftExtensionsConfigurationPackageVersion)
+ $(MicrosoftExtensionsConfigurationAbstractionsPackageVersion)
+ $(MicrosoftExtensionsConfigurationBinderPackageVersion)
+ $(MicrosoftExtensionsConfigurationCommandLinePackageVersion)
+ $(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)
+ $(MicrosoftExtensionsConfigurationFileExtensionsPackageVersion)
+ $(MicrosoftExtensionsConfigurationIniPackageVersion)
+ $(MicrosoftExtensionsConfigurationJsonPackageVersion)
+ $(MicrosoftExtensionsConfigurationUserSecretsPackageVersion)
+ $(MicrosoftExtensionsConfigurationXmlPackageVersion)
+ $(MicrosoftExtensionsDependencyInjectionPackageVersion)
+ $(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)
+ $(MicrosoftExtensionsDependencyModelPackageVersion)
+ $(MicrosoftExtensionsDiagnosticsPackageVersion)
+ $(MicrosoftExtensionsDiagnosticsAbstractionsPackageVersion)
+ $(MicrosoftExtensionsFileProvidersAbstractionsPackageVersion)
+ $(MicrosoftExtensionsFileProvidersCompositePackageVersion)
+ $(MicrosoftExtensionsFileProvidersPhysicalPackageVersion)
+ $(MicrosoftExtensionsFileSystemGlobbingPackageVersion)
+ $(MicrosoftExtensionsHostFactoryResolverSourcesPackageVersion)
+ $(MicrosoftExtensionsHostingPackageVersion)
+ $(MicrosoftExtensionsHostingAbstractionsPackageVersion)
+ $(MicrosoftExtensionsHttpPackageVersion)
+ $(MicrosoftExtensionsLoggingPackageVersion)
+ $(MicrosoftExtensionsLoggingAbstractionsPackageVersion)
+ $(MicrosoftExtensionsLoggingConfigurationPackageVersion)
+ $(MicrosoftExtensionsLoggingConsolePackageVersion)
+ $(MicrosoftExtensionsLoggingDebugPackageVersion)
+ $(MicrosoftExtensionsLoggingEventLogPackageVersion)
+ $(MicrosoftExtensionsLoggingEventSourcePackageVersion)
+ $(MicrosoftExtensionsLoggingTraceSourcePackageVersion)
+ $(MicrosoftExtensionsOptionsPackageVersion)
+ $(MicrosoftExtensionsOptionsConfigurationExtensionsPackageVersion)
+ $(MicrosoftExtensionsOptionsDataAnnotationsPackageVersion)
+ $(MicrosoftExtensionsPrimitivesPackageVersion)
+ $(MicrosoftInternalRuntimeAspNetCoreTransportPackageVersion)
+ $(MicrosoftNETRuntimeMonoAOTCompilerTaskPackageVersion)
+ $(MicrosoftNETRuntimeWebAssemblySdkPackageVersion)
+ $(MicrosoftNETCoreAppRefPackageVersion)
+ $(MicrosoftNETCoreBrowserDebugHostTransportPackageVersion)
+ $(MicrosoftNETCorePlatformsPackageVersion)
+ $(MicrosoftWebXdtPackageVersion)
+ $(SystemCollectionsImmutablePackageVersion)
+ $(SystemCompositionPackageVersion)
+ $(SystemConfigurationConfigurationManagerPackageVersion)
+ $(SystemDiagnosticsDiagnosticSourcePackageVersion)
+ $(SystemDiagnosticsEventLogPackageVersion)
+ $(SystemDiagnosticsPerformanceCounterPackageVersion)
+ $(SystemDirectoryServicesProtocolsPackageVersion)
+ $(SystemFormatsAsn1PackageVersion)
+ $(SystemFormatsCborPackageVersion)
+ $(SystemIOHashingPackageVersion)
+ $(SystemIOPipelinesPackageVersion)
+ $(SystemMemoryDataPackageVersion)
+ $(SystemNetHttpJsonPackageVersion)
+ $(SystemNetHttpWinHttpHandlerPackageVersion)
+ $(SystemNetServerSentEventsPackageVersion)
+ $(SystemNumericsTensorsPackageVersion)
+ $(SystemReflectionMetadataPackageVersion)
+ $(SystemResourcesExtensionsPackageVersion)
+ $(SystemRuntimeCachingPackageVersion)
+ $(SystemSecurityCryptographyPkcsPackageVersion)
+ $(SystemSecurityCryptographyXmlPackageVersion)
+ $(SystemSecurityPermissionsPackageVersion)
+ $(SystemServiceProcessServiceControllerPackageVersion)
+ $(SystemTextEncodingsWebPackageVersion)
+ $(SystemTextJsonPackageVersion)
+ $(SystemThreadingAccessControlPackageVersion)
+ $(SystemThreadingChannelsPackageVersion)
+ $(SystemThreadingRateLimitingPackageVersion)
+
+ $(MicrosoftCodeAnalysisCommonPackageVersion)
+ $(MicrosoftCodeAnalysisCSharpPackageVersion)
+ $(MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion)
+ $(MicrosoftCodeAnalysisExternalAccessAspNetCorePackageVersion)
+
+ $(MicrosoftExtensionsCachingHybridPackageVersion)
+ $(MicrosoftExtensionsDiagnosticsTestingPackageVersion)
+ $(MicrosoftExtensionsTimeProviderTestingPackageVersion)
+
+ $(optimizationlinuxarm64MIBCRuntimePackageVersion)
+ $(optimizationlinuxx64MIBCRuntimePackageVersion)
+ $(optimizationwindows_ntarm64MIBCRuntimePackageVersion)
+ $(optimizationwindows_ntx64MIBCRuntimePackageVersion)
+ $(optimizationwindows_ntx86MIBCRuntimePackageVersion)
+
+ $(MicrosoftBuildPackageVersion)
+ $(MicrosoftBuildFrameworkPackageVersion)
+ $(MicrosoftBuildTasksCorePackageVersion)
+ $(MicrosoftBuildUtilitiesCorePackageVersion)
+
+ $(NuGetFrameworksPackageVersion)
+ $(NuGetPackagingPackageVersion)
+ $(NuGetVersioningPackageVersion)
+
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index ab3a9fd9cb29..b4b82aa78905 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -390,17 +390,17 @@
https://github.com/dotnet/dotnet
6a953e76162f3f079405f80e28664fa51b136740
-
+
https://github.com/dotnet/extensions
- 3f10dd4d52002f1d83435f7ca791b77fea1718e6
+ c6529a0a68989cc881e4add4872c344917bc1ca9
-
+
https://github.com/dotnet/extensions
- 3f10dd4d52002f1d83435f7ca791b77fea1718e6
+ c6529a0a68989cc881e4add4872c344917bc1ca9
-
+
https://github.com/dotnet/extensions
- 3f10dd4d52002f1d83435f7ca791b77fea1718e6
+ c6529a0a68989cc881e4add4872c344917bc1ca9
https://dev.azure.com/dnceng/internal/_git/dotnet-optimization
diff --git a/eng/Versions.props b/eng/Versions.props
index bcbabaac12a1..d57f9aad228c 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -5,6 +5,7 @@
-->
+
10
0
@@ -56,126 +57,6 @@
false
-
-
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
-
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
-
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
- 10.0.0-preview.7.25377.103
-
- 9.8.0-preview.1.25374.2
- 9.8.0-preview.1.25374.2
- 9.8.0-preview.1.25374.2
-
- 10.0.0-rc.1.25377.103
- 10.0.0-rc.1.25377.103
- 10.0.0-rc.1.25377.103
- 10.0.0-rc.1.25377.103
- 10.0.0-rc.1.25377.103
- 10.0.0-rc.1.25377.103
- 10.0.0-rc.1.25377.103
- 10.0.0-rc.1.25377.103
-
- 10.0.0-beta.25377.103
- 10.0.0-beta.25377.103
- 10.0.0-beta.25377.103
- 10.0.0-beta.25377.103
-
- 3.2.0-preview.25377.103
-
- 1.0.0-prerelease.25374.3
- 1.0.0-prerelease.25374.3
- 1.0.0-prerelease.25374.3
- 1.0.0-prerelease.25374.3
- 1.0.0-prerelease.25374.3
-
-
@@ -269,10 +150,6 @@
9.0.0-rtm-24529-3
$(MicrosoftAspNetCoreAzureAppServicesSiteExtension90Version)
$(MicrosoftAspNetCoreAzureAppServicesSiteExtension90Version)
-
- 6.2.4
- 6.2.4
- 6.2.4
1.11.4
0.9.9
diff --git a/eng/scripts/CodeCheck.ps1 b/eng/scripts/CodeCheck.ps1
index 31e1294d773e..90e04810f19f 100644
--- a/eng/scripts/CodeCheck.ps1
+++ b/eng/scripts/CodeCheck.ps1
@@ -107,68 +107,6 @@ try {
"Packages in package-lock.json file resolved from wrong registry. All dependencies must be resolved from $registry"
}
- #
- # Versions.props and Version.Details.xml
- #
-
- Write-Host "Checking that Versions.props and Version.Details.xml match"
- [xml] $versionProps = Get-Content "$repoRoot/eng/Versions.props"
- [xml] $versionDetails = Get-Content "$repoRoot/eng/Version.Details.xml"
- $globalJson = Get-Content $repoRoot/global.json | ConvertFrom-Json
-
- $versionVars = New-Object 'System.Collections.Generic.HashSet[string]'
- foreach ($vars in $versionProps.SelectNodes("//PropertyGroup[`@Label=`"Automated`"]/*")) {
- $versionVars.Add($vars.Name) | Out-Null
- }
-
- foreach ($dep in $versionDetails.SelectNodes('//Dependency')) {
- Write-Verbose "Found $dep"
-
- if ($dep.Label -eq 'Manual') {
- # skip dependencies that are manually updated
- continue
- }
-
- $expectedVersion = $dep.Version
-
- if ($dep.Name -in $globalJson.'msbuild-sdks'.PSObject.Properties.Name) {
- $actualVersion = $globalJson.'msbuild-sdks'.($dep.Name)
-
- if ($expectedVersion -ne $actualVersion) {
- LogError -filepath "$repoRoot\global.json" `
- ("MSBuild SDK version '$($dep.Name)' in global.json does not match the value in " +
- "Version.Details.xml. Expected '$expectedVersion', actual '$actualVersion'")
- }
- }
- else {
- $varName = $dep.Name -replace '\.',''
- $varName = $varName -replace '\-',''
- $varName = "${varName}Version"
-
- $versionVar = $versionProps.SelectSingleNode("//PropertyGroup[`@Label=`"Automated`"]/$varName")
- $actualVersion = $versionVar.InnerText
- $versionVars.Remove($varName) | Out-Null
-
- if (-not $versionVar) {
- LogError "Missing version variable '$varName' in the 'Automated' property group in $repoRoot/eng/Versions.props"
- continue
- }
-
- if ($expectedVersion -ne $actualVersion) {
- LogError -filepath "$repoRoot\eng\Versions.props" `
- ("Version variable '$varName' does not match the value in Version.Details.xml. " +
- "Expected '$expectedVersion', actual '$actualVersion'")
- }
- }
- }
-
- foreach ($unexpectedVar in $versionVars) {
- LogError -Filepath "$repoRoot\eng\Versions.props" `
- ("Version variable '$unexpectedVar' does not have a matching entry in Version.Details.xml. " +
- "See https://github.com/dotnet/aspnetcore/blob/main/docs/ReferenceResolution.md for instructions " +
- "on how to add a new dependency.")
- }
-
# ComponentsWebAssembly-CSharp.sln is used by the templating engine; MessagePack.sln is irrelevant (in submodule).
$solution = Get-ChildItem "$repoRoot/AspNetCore.slnx"
$solutionFile = Split-Path -Leaf $solution
diff --git a/src/Components/Analyzers/src/ComponentFacts.cs b/src/Components/Analyzers/src/ComponentFacts.cs
index 43561128b0a5..f7f976dce6dc 100644
--- a/src/Components/Analyzers/src/ComponentFacts.cs
+++ b/src/Components/Analyzers/src/ComponentFacts.cs
@@ -87,6 +87,57 @@ public static bool IsCascadingParameter(ComponentSymbols symbols, IPropertySymbo
return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.CascadingParameterAttribute));
}
+ public static bool IsSupplyParameterFromForm(ComponentSymbols symbols, IPropertySymbol property)
+ {
+ if (symbols == null)
+ {
+ throw new ArgumentNullException(nameof(symbols));
+ }
+
+ if (property == null)
+ {
+ throw new ArgumentNullException(nameof(property));
+ }
+
+ if (symbols.SupplyParameterFromFormAttribute == null)
+ {
+ return false;
+ }
+
+ return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.SupplyParameterFromFormAttribute));
+ }
+
+ public static bool IsComponentBase(ComponentSymbols symbols, INamedTypeSymbol type)
+ {
+ if (symbols is null)
+ {
+ throw new ArgumentNullException(nameof(symbols));
+ }
+
+ if (type is null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ if (symbols.ComponentBaseType == null)
+ {
+ return false;
+ }
+
+ // Check if the type inherits from ComponentBase
+ var current = type.BaseType;
+ while (current != null)
+ {
+ if (SymbolEqualityComparer.Default.Equals(current, symbols.ComponentBaseType))
+ {
+ return true;
+ }
+ current = current.BaseType;
+ }
+
+ return false;
+ }
+
public static bool IsComponent(ComponentSymbols symbols, Compilation compilation, INamedTypeSymbol type)
{
if (symbols is null)
diff --git a/src/Components/Analyzers/src/ComponentSymbols.cs b/src/Components/Analyzers/src/ComponentSymbols.cs
index ccdca61d9749..e28da74f28d8 100644
--- a/src/Components/Analyzers/src/ComponentSymbols.cs
+++ b/src/Components/Analyzers/src/ComponentSymbols.cs
@@ -47,9 +47,15 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
var parameterCaptureUnmatchedValuesRuntimeType = dictionary.Construct(@string, @object);
+ // Try to get optional symbols for SupplyParameterFromForm analyzer
+ var supplyParameterFromFormAttribute = compilation.GetTypeByMetadataName(ComponentsApi.SupplyParameterFromFormAttribute.MetadataName);
+ var componentBaseType = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
+
symbols = new ComponentSymbols(
parameterAttribute,
cascadingParameterAttribute,
+ supplyParameterFromFormAttribute,
+ componentBaseType,
parameterCaptureUnmatchedValuesRuntimeType,
icomponentType);
return true;
@@ -58,11 +64,15 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
private ComponentSymbols(
INamedTypeSymbol parameterAttribute,
INamedTypeSymbol cascadingParameterAttribute,
+ INamedTypeSymbol supplyParameterFromFormAttribute,
+ INamedTypeSymbol componentBaseType,
INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType,
INamedTypeSymbol icomponentType)
{
ParameterAttribute = parameterAttribute;
CascadingParameterAttribute = cascadingParameterAttribute;
+ SupplyParameterFromFormAttribute = supplyParameterFromFormAttribute; // Can be null
+ ComponentBaseType = componentBaseType; // Can be null
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
IComponentType = icomponentType;
}
@@ -74,5 +84,9 @@ private ComponentSymbols(
public INamedTypeSymbol CascadingParameterAttribute { get; }
+ public INamedTypeSymbol SupplyParameterFromFormAttribute { get; } // Can be null if not available
+
+ public INamedTypeSymbol ComponentBaseType { get; } // Can be null if not available
+
public INamedTypeSymbol IComponentType { get; }
}
diff --git a/src/Components/Analyzers/src/ComponentsApi.cs b/src/Components/Analyzers/src/ComponentsApi.cs
index a62d070dedef..361e99b3e146 100644
--- a/src/Components/Analyzers/src/ComponentsApi.cs
+++ b/src/Components/Analyzers/src/ComponentsApi.cs
@@ -23,6 +23,18 @@ public static class CascadingParameterAttribute
public const string MetadataName = FullTypeName;
}
+ public static class SupplyParameterFromFormAttribute
+ {
+ public const string FullTypeName = "Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute";
+ public const string MetadataName = FullTypeName;
+ }
+
+ public static class ComponentBase
+ {
+ public const string FullTypeName = "Microsoft.AspNetCore.Components.ComponentBase";
+ public const string MetadataName = FullTypeName;
+ }
+
public static class IComponent
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.IComponent";
diff --git a/src/Components/Analyzers/src/DiagnosticDescriptors.cs b/src/Components/Analyzers/src/DiagnosticDescriptors.cs
index ea3332c56fc7..afba1e3acd50 100644
--- a/src/Components/Analyzers/src/DiagnosticDescriptors.cs
+++ b/src/Components/Analyzers/src/DiagnosticDescriptors.cs
@@ -74,4 +74,13 @@ internal static class DiagnosticDescriptors
Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+
+ public static readonly DiagnosticDescriptor SupplyParameterFromFormShouldNotHavePropertyInitializer = new(
+ "BL0008",
+ CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Title)),
+ CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Format)),
+ Usage,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Description)));
}
diff --git a/src/Components/Analyzers/src/Resources.resx b/src/Components/Analyzers/src/Resources.resx
index 5ef9f54b53eb..a6dc20636cc6 100644
--- a/src/Components/Analyzers/src/Resources.resx
+++ b/src/Components/Analyzers/src/Resources.resx
@@ -180,4 +180,13 @@
Component parameters should be auto properties
+
+ The value of a property decorated with [SupplyParameterFromForm] and initialized with a property initializer can be overwritten with null when the component receives parameters. To ensure the initialized value is not overwritten, move the initialization to a component lifecycle method like OnInitialized or OnInitializedAsync
+
+
+ Property '{0}' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.
+
+
+ Property with [SupplyParameterFromForm] should not have initializer
+
\ No newline at end of file
diff --git a/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs b/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs
new file mode 100644
index 000000000000..00bbbcaa1180
--- /dev/null
+++ b/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+#nullable enable
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class SupplyParameterFromFormAnalyzer : DiagnosticAnalyzer
+{
+ public SupplyParameterFromFormAnalyzer()
+ {
+ SupportedDiagnostics = ImmutableArray.Create(
+ DiagnosticDescriptors.SupplyParameterFromFormShouldNotHavePropertyInitializer);
+ }
+
+ public override ImmutableArray SupportedDiagnostics { get; }
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.RegisterCompilationStartAction(context =>
+ {
+ if (!ComponentSymbols.TryCreate(context.Compilation, out var symbols))
+ {
+ // Types we need are not defined.
+ return;
+ }
+
+ context.RegisterSyntaxNodeAction(context =>
+ {
+ var propertyDeclaration = (PropertyDeclarationSyntax)context.Node;
+
+ // Check if property has an initializer
+ if (propertyDeclaration.Initializer == null)
+ {
+ return;
+ }
+
+ // Ignore initializers that set to default values (null, default, etc.)
+ if (IsDefaultValueInitializer(propertyDeclaration.Initializer.Value))
+ {
+ return;
+ }
+
+ var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration);
+ if (propertySymbol == null)
+ {
+ return;
+ }
+
+ // Check if property has [SupplyParameterFromForm] attribute
+ if (!ComponentFacts.IsSupplyParameterFromForm(symbols, propertySymbol))
+ {
+ return;
+ }
+
+ // Check if the containing type inherits from ComponentBase
+ var containingType = propertySymbol.ContainingType;
+ if (!ComponentFacts.IsComponentBase(symbols, containingType))
+ {
+ return;
+ }
+
+ var propertyLocation = propertySymbol.Locations.FirstOrDefault();
+ if (propertyLocation != null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.SupplyParameterFromFormShouldNotHavePropertyInitializer,
+ propertyLocation,
+ propertySymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
+ }
+ }, SyntaxKind.PropertyDeclaration);
+ });
+ }
+
+ private static bool IsDefaultValueInitializer(ExpressionSyntax expression)
+ {
+ return expression switch
+ {
+ // null
+ LiteralExpressionSyntax { Token.ValueText: "null" } => true,
+ // null!
+ PostfixUnaryExpressionSyntax { Operand: LiteralExpressionSyntax { Token.ValueText: "null" }, OperatorToken.ValueText: "!" } => true,
+ // default
+ LiteralExpressionSyntax literal when literal.Token.IsKind(SyntaxKind.DefaultKeyword) => true,
+ // default!
+ PostfixUnaryExpressionSyntax { Operand: LiteralExpressionSyntax literal, OperatorToken.ValueText: "!" }
+ when literal.Token.IsKind(SyntaxKind.DefaultKeyword) => true,
+ _ => false
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
new file mode 100644
index 000000000000..549b3cf115ec
--- /dev/null
+++ b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
@@ -0,0 +1,243 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Test;
+
+public class SupplyParameterFromFormAnalyzerTest : DiagnosticVerifier
+{
+ protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new SupplyParameterFromFormAnalyzer();
+
+ private static readonly string TestDeclarations = $@"
+ namespace {typeof(ParameterAttribute).Namespace}
+ {{
+ public class {typeof(ParameterAttribute).Name} : System.Attribute
+ {{
+ public bool CaptureUnmatchedValues {{ get; set; }}
+ }}
+
+ public class {typeof(CascadingParameterAttribute).Name} : System.Attribute
+ {{
+ }}
+
+ public class SupplyParameterFromFormAttribute : System.Attribute
+ {{
+ public string Name {{ get; set; }}
+ public string FormName {{ get; set; }}
+ }}
+
+ public interface {typeof(IComponent).Name}
+ {{
+ }}
+
+ public abstract class ComponentBase : {typeof(IComponent).Name}
+ {{
+ }}
+ }}
+";
+
+ [Fact]
+ public void IgnoresPropertiesWithoutSupplyParameterFromFormAttribute()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ public string MyProperty {{ get; set; }} = ""initial-value"";
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithoutInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }}
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresNonComponentBaseClasses()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class NotAComponent
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""initial-value"";
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void ReportsWarningForSupplyParameterFromFormWithInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""initial-value"";
+ }}
+ }}" + TestDeclarations;
+
+ var expected = new DiagnosticResult
+ {
+ Id = "BL0008",
+ Message = "Property 'ConsoleApplication1.TestComponent.MyProperty' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 7, 53)
+ }
+ };
+
+ VerifyCSharpDiagnostic(test, expected);
+ }
+
+ [Fact]
+ public void ReportsWarningForSupplyParameterFromFormWithObjectInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public InputModel Input {{ get; set; }} = new InputModel();
+ }}
+
+ class InputModel
+ {{
+ public string Value {{ get; set; }} = """";
+ }}
+ }}" + TestDeclarations;
+
+ var expected = new DiagnosticResult
+ {
+ Id = "BL0008",
+ Message = "Property 'ConsoleApplication1.TestComponent.Input' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 7, 57)
+ }
+ };
+
+ VerifyCSharpDiagnostic(test, expected);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithNullInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = null;
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithNullForgivingInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = null!;
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithDefaultInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = default;
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithDefaultForgivingInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = default!;
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void WorksWithInheritedComponentBase()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class BaseComponent : ComponentBase
+ {{
+ }}
+
+ class TestComponent : BaseComponent
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""initial-value"";
+ }}
+ }}" + TestDeclarations;
+
+ var expected = new DiagnosticResult
+ {
+ Id = "BL0008",
+ Message = "Property 'ConsoleApplication1.TestComponent.MyProperty' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 11, 53)
+ }
+ };
+
+ VerifyCSharpDiagnostic(test, expected);
+ }
+}
\ No newline at end of file
diff --git a/src/Components/Components/src/ComponentSubscriptionKey.cs b/src/Components/Components/src/ComponentSubscriptionKey.cs
new file mode 100644
index 000000000000..010510f480a6
--- /dev/null
+++ b/src/Components/Components/src/ComponentSubscriptionKey.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.
+
+using System.Diagnostics;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace Microsoft.AspNetCore.Components.Infrastructure;
+
+[DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
+internal readonly struct ComponentSubscriptionKey(ComponentState subscriber, string propertyName) : IEquatable
+{
+ public ComponentState Subscriber { get; } = subscriber;
+
+ public string PropertyName { get; } = propertyName;
+
+ public bool Equals(ComponentSubscriptionKey other)
+ => Subscriber == other.Subscriber && PropertyName == other.PropertyName;
+
+ public override bool Equals(object? obj)
+ => obj is ComponentSubscriptionKey other && Equals(other);
+
+ public override int GetHashCode()
+ => HashCode.Combine(Subscriber, PropertyName);
+
+ private string GetDebuggerDisplay()
+ => $"{Subscriber.Component.GetType().Name}.{PropertyName}";
+}
diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs
index d3f5e9fd9309..1c7337eb534f 100644
--- a/src/Components/Components/src/PersistentComponentState.cs
+++ b/src/Components/Components/src/PersistentComponentState.cs
@@ -1,6 +1,7 @@
// 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;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
@@ -16,24 +17,30 @@ public class PersistentComponentState
private readonly IDictionary _currentState;
private readonly List _registeredCallbacks;
+ private readonly List _registeredRestoringCallbacks;
internal PersistentComponentState(
- IDictionary currentState,
- List pauseCallbacks)
+ IDictionary currentState,
+ List pauseCallbacks,
+ List restoringCallbacks)
{
_currentState = currentState;
_registeredCallbacks = pauseCallbacks;
+ _registeredRestoringCallbacks = restoringCallbacks;
}
internal bool PersistingState { get; set; }
- internal void InitializeExistingState(IDictionary existingState)
+ internal RestoreContext CurrentContext { get; private set; } = RestoreContext.InitialValue;
+
+ internal void InitializeExistingState(IDictionary existingState, RestoreContext context)
{
if (_existingState != null)
{
throw new InvalidOperationException("PersistentComponentState already initialized.");
}
_existingState = existingState ?? throw new ArgumentNullException(nameof(existingState));
+ CurrentContext = context;
}
///
@@ -68,6 +75,30 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call
return new PersistingComponentStateSubscription(_registeredCallbacks, persistenceCallback);
}
+ ///
+ /// Register a callback to restore the state when the application state is being restored.
+ ///
+ /// The callback to invoke when the application state is being restored.
+ /// Options that control the restoration behavior.
+ /// A subscription that can be used to unregister the callback when disposed.
+ public RestoringComponentStateSubscription RegisterOnRestoring(Action callback, RestoreOptions options)
+ {
+ Debug.Assert(CurrentContext != null);
+ if (CurrentContext.ShouldRestore(options))
+ {
+ callback();
+ }
+
+ if (options.AllowUpdates)
+ {
+ var registration = new RestoreComponentStateRegistration(callback);
+ _registeredRestoringCallbacks.Add(registration);
+ return new RestoringComponentStateSubscription(_registeredRestoringCallbacks, registration);
+ }
+
+ return default;
+ }
+
///
/// Serializes as JSON and persists it under the given .
///
@@ -214,4 +245,17 @@ private bool TryTake(string key, out byte[]? value)
return false;
}
}
+
+ internal void UpdateExistingState(IDictionary state, RestoreContext context)
+ {
+ ArgumentNullException.ThrowIfNull(state);
+
+ if (_existingState == null || _existingState.Count > 0)
+ {
+ throw new InvalidOperationException("Cannot update existing state: previous state has not been cleared or state is not initialized.");
+ }
+
+ _existingState = state;
+ CurrentContext = context;
+ }
}
diff --git a/src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs b/src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs
index 72c1ca666411..41fa00a6bb7c 100644
--- a/src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs
+++ b/src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs
@@ -12,9 +12,11 @@ namespace Microsoft.AspNetCore.Components.Infrastructure;
public class ComponentStatePersistenceManager
{
private readonly List _registeredCallbacks = new();
+ private readonly List _registeredRestoringCallbacks = new();
private readonly ILogger _logger;
private bool _stateIsPersisted;
+ private bool _stateIsInitialized;
private readonly PersistentServicesRegistry? _servicesRegistry;
private readonly Dictionary _currentState = new(StringComparer.Ordinal);
@@ -24,7 +26,7 @@ public class ComponentStatePersistenceManager
///
public ComponentStatePersistenceManager(ILogger logger)
{
- State = new PersistentComponentState(_currentState, _registeredCallbacks);
+ State = new PersistentComponentState(_currentState, _registeredCallbacks, _registeredRestoringCallbacks);
_logger = logger;
}
@@ -55,10 +57,38 @@ public ComponentStatePersistenceManager(ILoggerThe to restore the application state from.
/// A that will complete when the state has been restored.
public async Task RestoreStateAsync(IPersistentComponentStateStore store)
+ {
+ await RestoreStateAsync(store, RestoreContext.InitialValue);
+ }
+
+ ///
+ /// Restores the application state.
+ ///
+ /// The to restore the application state from.
+ /// The that provides additional context for the restoration.
+ /// A that will complete when the state has been restored.
+ public async Task RestoreStateAsync(IPersistentComponentStateStore store, RestoreContext context)
{
var data = await store.GetPersistedStateAsync();
- State.InitializeExistingState(data);
- _servicesRegistry?.Restore(State);
+
+ if (_stateIsInitialized)
+ {
+ if (context != RestoreContext.ValueUpdate)
+ {
+ throw new InvalidOperationException("State already initialized.");
+ }
+ State.UpdateExistingState(data, context);
+ foreach (var registration in _registeredRestoringCallbacks)
+ {
+ registration.Callback();
+ }
+ }
+ else
+ {
+ State.InitializeExistingState(data, context);
+ _servicesRegistry?.RegisterForPersistence(State);
+ _stateIsInitialized = true;
+ }
}
///
@@ -78,9 +108,6 @@ public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer ren
async Task PauseAndPersistState()
{
- // Ensure that we register the services before we start persisting the state.
- _servicesRegistry?.RegisterForPersistence(State);
-
State.PersistingState = true;
if (store is IEnumerable compositeStore)
@@ -271,4 +298,5 @@ static async Task AnyTaskFailed(List> pendingCallbackTasks)
return true;
}
}
+
}
diff --git a/src/Components/Components/src/PersistComponentStateRegistration.cs b/src/Components/Components/src/PersistentState/PersistComponentStateRegistration.cs
similarity index 100%
rename from src/Components/Components/src/PersistComponentStateRegistration.cs
rename to src/Components/Components/src/PersistentState/PersistComponentStateRegistration.cs
diff --git a/src/Components/Components/src/PersistentComponentStateSerializer.cs b/src/Components/Components/src/PersistentState/PersistentComponentStateSerializer.cs
similarity index 100%
rename from src/Components/Components/src/PersistentComponentStateSerializer.cs
rename to src/Components/Components/src/PersistentState/PersistentComponentStateSerializer.cs
diff --git a/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs b/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs
index b072cd4c2c88..e27e5c2560b0 100644
--- a/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs
+++ b/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs
@@ -18,11 +18,11 @@ namespace Microsoft.AspNetCore.Components.Infrastructure;
internal sealed class PersistentServicesRegistry
{
private static readonly string _registryKey = typeof(PersistentServicesRegistry).FullName!;
- private static readonly RootTypeCache _persistentServiceTypeCache = new RootTypeCache();
+ private static readonly RootTypeCache _persistentServiceTypeCache = new();
private readonly IServiceProvider _serviceProvider;
private IPersistentServiceRegistration[] _registrations;
- private List _subscriptions = [];
+ private List<(PersistingComponentStateSubscription, RestoringComponentStateSubscription)> _subscriptions = [];
private static readonly ConcurrentDictionary _cachedAccessorsByType = new();
static PersistentServicesRegistry()
@@ -54,7 +54,9 @@ internal void RegisterForPersistence(PersistentComponentState state)
return;
}
- var subscriptions = new List(_registrations.Length + 1);
+ UpdateRegistrations(state);
+ var subscriptions = new List<(PersistingComponentStateSubscription, RestoringComponentStateSubscription)>(
+ _registrations.Length + 1);
for (var i = 0; i < _registrations.Length; i++)
{
var registration = _registrations[i];
@@ -67,20 +69,29 @@ internal void RegisterForPersistence(PersistentComponentState state)
var renderMode = registration.GetRenderModeOrDefault();
var instance = _serviceProvider.GetRequiredService(type);
- subscriptions.Add(state.RegisterOnPersisting(() =>
- {
- PersistInstanceState(instance, type, state);
- return Task.CompletedTask;
- }, renderMode));
+ subscriptions.Add((
+ state.RegisterOnPersisting(() =>
+ {
+ PersistInstanceState(instance, type, state);
+ return Task.CompletedTask;
+ }, renderMode),
+ // In order to avoid registering one callback per property, we register a single callback with the most
+ // permissive options and perform the filtering inside of it.
+ state.RegisterOnRestoring(() =>
+ {
+ RestoreInstanceState(instance, type, state);
+ }, new RestoreOptions { AllowUpdates = true })));
}
if (RenderMode != null)
{
- subscriptions.Add(state.RegisterOnPersisting(() =>
- {
- state.PersistAsJson(_registryKey, _registrations);
- return Task.CompletedTask;
- }, RenderMode));
+ subscriptions.Add((
+ state.RegisterOnPersisting(() =>
+ {
+ state.PersistAsJson(_registryKey, _registrations);
+ return Task.CompletedTask;
+ }, RenderMode),
+ default));
}
_subscriptions = subscriptions;
@@ -92,7 +103,7 @@ private static void PersistInstanceState(object instance, Type type, PersistentC
var accessors = _cachedAccessorsByType.GetOrAdd(instance.GetType(), static (runtimeType, declaredType) => new PropertiesAccessor(runtimeType, declaredType), type);
foreach (var (key, propertyType) in accessors.KeyTypePairs)
{
- var (setter, getter) = accessors.GetAccessor(key);
+ var (setter, getter, options) = accessors.GetAccessor(key);
var value = getter.GetValue(instance);
if (value != null)
{
@@ -105,33 +116,12 @@ private static void PersistInstanceState(object instance, Type type, PersistentC
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Types registered for persistence are preserved in the API call to register them and typically live in assemblies that aren't trimmed.")]
[DynamicDependency(LinkerFlags.JsonSerialized, typeof(PersistentServiceRegistration))]
- internal void Restore(PersistentComponentState state)
+ private void UpdateRegistrations(PersistentComponentState state)
{
if (state.TryTakeFromJson(_registryKey, out var registry) && registry != null)
{
_registrations = ResolveRegistrations(_registrations.Concat(registry));
}
-
- RestoreRegistrationsIfAvailable(state);
- }
-
- [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Types registered for persistence are preserved in the API call to register them and typically live in assemblies that aren't trimmed.")]
- private void RestoreRegistrationsIfAvailable(PersistentComponentState state)
- {
- foreach (var registration in _registrations)
- {
- var type = ResolveType(registration);
- if (type == null)
- {
- continue;
- }
-
- var instance = _serviceProvider.GetService(type);
- if (instance != null)
- {
- RestoreInstanceState(instance, type, state);
- }
- }
}
[RequiresUnreferencedCode("Calls Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(String, Type, out Object)")]
@@ -140,9 +130,13 @@ private static void RestoreInstanceState(object instance, Type type, PersistentC
var accessors = _cachedAccessorsByType.GetOrAdd(instance.GetType(), static (runtimeType, declaredType) => new PropertiesAccessor(runtimeType, declaredType), type);
foreach (var (key, propertyType) in accessors.KeyTypePairs)
{
+ var (setter, getter, options) = accessors.GetAccessor(key);
+ if (!state.CurrentContext.ShouldRestore(options))
+ {
+ continue;
+ }
if (state.TryTakeFromJson(key, propertyType, out var result))
{
- var (setter, getter) = accessors.GetAccessor(key);
setter.SetValue(instance, result!);
}
}
@@ -165,12 +159,12 @@ private sealed class PropertiesAccessor
{
internal const BindingFlags BindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
- private readonly Dictionary _underlyingAccessors;
+ private readonly Dictionary _underlyingAccessors;
private readonly (string, Type)[] _cachedKeysForService;
public PropertiesAccessor([DynamicallyAccessedMembers(LinkerFlags.Component)] Type targetType, Type keyType)
{
- _underlyingAccessors = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _underlyingAccessors = new Dictionary(StringComparer.OrdinalIgnoreCase);
var keys = new List<(string, Type)>();
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
@@ -204,10 +198,16 @@ public PropertiesAccessor([DynamicallyAccessedMembers(LinkerFlags.Component)] Ty
$"The type '{targetType.FullName}' declares a property matching the name '{propertyName}' that is not public. Persistent service properties must be public.");
}
+ var restoreOptions = new RestoreOptions
+ {
+ RestoreBehavior = parameterAttribute.RestoreBehavior,
+ AllowUpdates = parameterAttribute.AllowUpdates,
+ };
+
var propertySetter = new PropertySetter(targetType, propertyInfo);
var propertyGetter = new PropertyGetter(targetType, propertyInfo);
- _underlyingAccessors.Add(key, (propertySetter, propertyGetter));
+ _underlyingAccessors.Add(key, (propertySetter, propertyGetter, restoreOptions));
}
_cachedKeysForService = [.. keys];
@@ -236,7 +236,7 @@ internal static IEnumerable GetCandidateBindableProperties(
[DynamicallyAccessedMembers(LinkerFlags.Component)] Type targetType)
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, BindablePropertyFlags);
- internal (PropertySetter setter, PropertyGetter getter) GetAccessor(string key) =>
+ internal (PropertySetter setter, PropertyGetter getter, RestoreOptions options) GetAccessor(string key) =>
_underlyingAccessors.TryGetValue(key, out var result) ? result : default;
}
diff --git a/src/Components/Components/src/PersistentStateProviderServiceCollectionExtensions.cs b/src/Components/Components/src/PersistentState/PersistentStateProviderServiceCollectionExtensions.cs
similarity index 100%
rename from src/Components/Components/src/PersistentStateProviderServiceCollectionExtensions.cs
rename to src/Components/Components/src/PersistentState/PersistentStateProviderServiceCollectionExtensions.cs
diff --git a/src/Components/Components/src/PersistentState/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentState/PersistentStateValueProvider.cs
new file mode 100644
index 000000000000..53158b44e0db
--- /dev/null
+++ b/src/Components/Components/src/PersistentState/PersistentStateValueProvider.cs
@@ -0,0 +1,63 @@
+// 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 Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Components.Infrastructure;
+
+internal sealed partial class PersistentStateValueProvider(PersistentComponentState state, ILogger logger, IServiceProvider serviceProvider) : ICascadingValueSupplier
+{
+ private readonly Dictionary _subscriptions = [];
+
+ public bool IsFixed => false;
+ // For testing purposes only
+ internal Dictionary Subscriptions => _subscriptions;
+
+ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
+ => parameterInfo.Attribute is PersistentStateAttribute;
+
+ [UnconditionalSuppressMessage(
+ "ReflectionAnalysis",
+ "IL2026:RequiresUnreferencedCode message",
+ Justification = "JSON serialization and deserialization might require types that cannot be statically analyzed.")]
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
+ Justification = "JSON serialization and deserialization might require types that cannot be statically analyzed.")]
+ public object? GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo)
+ {
+ var componentState = (ComponentState)key!;
+
+ if (_subscriptions.TryGetValue(new(componentState, parameterInfo.PropertyName), out var subscription))
+ {
+ return subscription.GetOrComputeLastValue();
+ }
+
+ return null;
+ }
+
+ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
+ {
+ var propertyName = parameterInfo.PropertyName;
+
+ var componentSubscription = new PersistentValueProviderComponentSubscription(
+ state,
+ subscriber,
+ parameterInfo,
+ serviceProvider,
+ logger);
+
+ _subscriptions.Add(new ComponentSubscriptionKey(subscriber, propertyName), componentSubscription);
+ }
+
+ public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
+ {
+ if (_subscriptions.TryGetValue(new(subscriber, parameterInfo.PropertyName), out var subscription))
+ {
+ subscription.Dispose();
+ _subscriptions.Remove(new(subscriber, parameterInfo.PropertyName));
+ }
+ }
+}
diff --git a/src/Components/Components/src/PersistentState/PersistentStateValueProviderKeyResolver.cs b/src/Components/Components/src/PersistentState/PersistentStateValueProviderKeyResolver.cs
new file mode 100644
index 000000000000..8de18363342c
--- /dev/null
+++ b/src/Components/Components/src/PersistentState/PersistentStateValueProviderKeyResolver.cs
@@ -0,0 +1,214 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Globalization;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Components.HotReload;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace Microsoft.AspNetCore.Components.Infrastructure;
+
+internal static class PersistentStateValueProviderKeyResolver
+{
+ private static readonly ConcurrentDictionary<(string, string, string), byte[]> _keyCache = new();
+
+ static PersistentStateValueProviderKeyResolver()
+ {
+ if (HotReloadManager.Default.MetadataUpdateSupported)
+ {
+ HotReloadManager.Default.OnDeltaApplied += ClearCaches;
+ }
+ }
+
+ private static void ClearCaches()
+ {
+ _keyCache.Clear();
+ }
+
+ // Internal for testing only
+ internal static string ComputeKey(ComponentState componentState, string propertyName)
+ {
+ // We need to come up with a pseudo-unique key for the storage key.
+ // We need to consider the property name, the component type, and its position within the component tree.
+ // If only one component of a given type is present on the page, then only the component type + property name is enough.
+ // If multiple components of the same type are present on the page, then we need to consider the position within the tree.
+ // To do that, we are going to use the `@key` directive on the component if present and if we deem it serializable.
+ // Serializable keys are Guid, DateOnly, TimeOnly, and any primitive type.
+ // The key is composed of four segments:
+ // Parent component type
+ // Component type
+ // Property name
+ // @key directive if present and serializable.
+ // We combine the first three parts into an identifier, and then we generate a derived identifier with the key
+ // We do it this way because the information for the first three pieces of data is static for the lifetime of the
+ // program and can be cached on each situation.
+
+ var parentComponentType = GetParentComponentType(componentState);
+ var componentType = GetComponentType(componentState);
+
+ var preKey = _keyCache.GetOrAdd((parentComponentType, componentType, propertyName), KeyFactory);
+ var finalKey = ComputeFinalKey(preKey, componentState);
+
+ return finalKey;
+ }
+
+ private static string ComputeFinalKey(byte[] preKey, ComponentState componentState)
+ {
+ Span keyHash = stackalloc byte[SHA256.HashSizeInBytes];
+
+ var key = GetSerializableKey(componentState);
+ byte[]? pool = null;
+ try
+ {
+ Span keyBuffer = stackalloc byte[1024];
+ var currentBuffer = keyBuffer;
+ preKey.CopyTo(keyBuffer);
+ if (key is IUtf8SpanFormattable spanFormattable)
+ {
+ var wroteKey = false;
+ while (!wroteKey)
+ {
+ currentBuffer = keyBuffer[preKey.Length..];
+ wroteKey = spanFormattable.TryFormat(currentBuffer, out var written, "", CultureInfo.InvariantCulture);
+ if (!wroteKey)
+ {
+ // It is really unlikely that we will enter here, but we need to handle this case
+ Debug.Assert(written == 0);
+ GrowBuffer(ref pool, ref keyBuffer);
+ }
+ else
+ {
+ currentBuffer = currentBuffer[..written];
+ }
+ }
+ }
+ else
+ {
+ var keySpan = ResolveKeySpan(key);
+ var wroteKey = false;
+ while (!wroteKey)
+ {
+ currentBuffer = keyBuffer[preKey.Length..];
+ wroteKey = Encoding.UTF8.TryGetBytes(keySpan, currentBuffer, out var written);
+ if (!wroteKey)
+ {
+ // It is really unlikely that we will enter here, but we need to handle this case
+ Debug.Assert(written == 0);
+ // Since this is utf-8, grab a buffer the size of the key * 4 + the preKey size
+ // this guarantees we have enough space to encode the key
+ GrowBuffer(ref pool, ref keyBuffer, keySpan.Length * 4 + preKey.Length);
+ }
+ else
+ {
+ currentBuffer = currentBuffer[..written];
+ }
+ }
+ }
+
+ keyBuffer = keyBuffer[..(preKey.Length + currentBuffer.Length)];
+
+ var hashSucceeded = SHA256.TryHashData(keyBuffer, keyHash, out _);
+ Debug.Assert(hashSucceeded);
+ return Convert.ToBase64String(keyHash);
+ }
+ finally
+ {
+ if (pool != null)
+ {
+ ArrayPool.Shared.Return(pool, clearArray: true);
+ }
+ }
+ }
+
+ private static ReadOnlySpan ResolveKeySpan(object? key)
+ {
+ if (key is IFormattable formattable)
+ {
+ var keyString = formattable.ToString("", CultureInfo.InvariantCulture);
+ return keyString.AsSpan();
+ }
+ else if (key is IConvertible convertible)
+ {
+ var keyString = convertible.ToString(CultureInfo.InvariantCulture);
+ return keyString.AsSpan();
+ }
+ return default;
+ }
+
+ private static void GrowBuffer(ref byte[]? pool, ref Span keyBuffer, int? size = null)
+ {
+ var newPool = pool == null ? ArrayPool.Shared.Rent(size ?? 2048) : ArrayPool.Shared.Rent(pool.Length * 2);
+ keyBuffer.CopyTo(newPool);
+ keyBuffer = newPool;
+ if (pool != null)
+ {
+ ArrayPool.Shared.Return(pool, clearArray: true);
+ }
+ pool = newPool;
+ }
+
+ private static object? GetSerializableKey(ComponentState componentState)
+ {
+ var componentKey = componentState.GetComponentKey();
+ if (componentKey != null && IsSerializableKey(componentKey))
+ {
+ return componentKey;
+ }
+
+ return null;
+ }
+
+ private static string GetComponentType(ComponentState componentState) => componentState.Component.GetType().FullName!;
+
+ private static string GetParentComponentType(ComponentState componentState)
+ {
+ if (componentState.ParentComponentState == null)
+ {
+ return "";
+ }
+ if (componentState.ParentComponentState.Component == null)
+ {
+ return "";
+ }
+
+ if (componentState.ParentComponentState.ParentComponentState != null)
+ {
+ var renderer = componentState.Renderer;
+ var parentRenderMode = renderer.GetComponentRenderMode(componentState.ParentComponentState.Component);
+ var grandParentRenderMode = renderer.GetComponentRenderMode(componentState.ParentComponentState.ParentComponentState.Component);
+ if (parentRenderMode != grandParentRenderMode)
+ {
+ // This is the case when EndpointHtmlRenderer introduces an SSRRenderBoundary component.
+ // We want to return "" because the SSRRenderBoundary component is not a real component
+ // and won't appear on the component tree in the WebAssemblyRenderer and RemoteRenderer
+ // interactive scenarios.
+ return "";
+ }
+ }
+
+ return GetComponentType(componentState.ParentComponentState);
+ }
+
+ private static byte[] KeyFactory((string parentComponentType, string componentType, string propertyName) parts) =>
+ SHA256.HashData(Encoding.UTF8.GetBytes(string.Join(".", parts.parentComponentType, parts.componentType, parts.propertyName)));
+
+ private static bool IsSerializableKey(object key)
+ {
+ if (key == null)
+ {
+ return false;
+ }
+ var keyType = key.GetType();
+ var result = Type.GetTypeCode(keyType) != TypeCode.Object
+ || keyType == typeof(Guid)
+ || keyType == typeof(DateTimeOffset)
+ || keyType == typeof(DateOnly)
+ || keyType == typeof(TimeOnly);
+
+ return result;
+ }
+}
diff --git a/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs b/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs
new file mode 100644
index 000000000000..30e537a418a3
--- /dev/null
+++ b/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs
@@ -0,0 +1,264 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using Microsoft.AspNetCore.Components.HotReload;
+using Microsoft.AspNetCore.Components.Reflection;
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Components.Infrastructure;
+
+internal partial class PersistentValueProviderComponentSubscription : IDisposable
+{
+ private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new();
+ private static readonly ConcurrentDictionary _serializerCache = new();
+ private static readonly object _uninitializedSentinel = new();
+
+ static PersistentValueProviderComponentSubscription()
+ {
+ if (HotReloadManager.Default.MetadataUpdateSupported)
+ {
+ HotReloadManager.Default.OnDeltaApplied += ClearCaches;
+ }
+ }
+
+ private static void ClearCaches()
+ {
+ _propertyGetterCache.Clear();
+ _serializerCache.Clear();
+ }
+
+ private readonly PersistentComponentState _state;
+ private readonly ComponentState _subscriber;
+ private readonly string _propertyName;
+ private readonly Type _propertyType;
+ private readonly PropertyGetter _propertyGetter;
+ private readonly IPersistentComponentStateSerializer? _customSerializer;
+ private readonly ILogger _logger;
+
+ private readonly PersistingComponentStateSubscription? _persistingSubscription;
+ private readonly RestoringComponentStateSubscription? _restoringSubscription;
+ private object? _lastValue = _uninitializedSentinel;
+ private bool _hasPendingInitialValue;
+ private bool _ignoreComponentPropertyValue;
+ private string? _storageKey;
+
+ public PersistentValueProviderComponentSubscription(
+ PersistentComponentState state,
+ ComponentState subscriber,
+ CascadingParameterInfo parameterInfo,
+ IServiceProvider serviceProvider,
+ ILogger logger)
+ {
+ _state = state;
+ _subscriber = subscriber;
+ _propertyName = parameterInfo.PropertyName;
+ _propertyType = parameterInfo.PropertyType;
+ _logger = logger;
+ var attribute = (PersistentStateAttribute)parameterInfo.Attribute;
+
+ _customSerializer = _serializerCache.GetOrAdd(_propertyType, SerializerFactory, serviceProvider);
+ _propertyGetter = _propertyGetterCache.GetOrAdd((subscriber.Component.GetType(), _propertyName), PropertyGetterFactory);
+
+ _persistingSubscription = state.RegisterOnPersisting(
+ PersistProperty,
+ subscriber.Renderer.GetComponentRenderMode(subscriber.Component));
+
+ _restoringSubscription = state.RegisterOnRestoring(
+ RestoreProperty,
+ new RestoreOptions { RestoreBehavior = attribute.RestoreBehavior, AllowUpdates = attribute.AllowUpdates });
+ }
+
+ // GetOrComputeLastValue is a bit of a special provider.
+ // Right after a Restore operation it will capture the last value and return that, but it must support the user
+ // overriding the property at a later point, so to support that, we need to keep track of whether or not we have
+ // delivered the last value, and if so, instead of returning the _lastValue, we simply read the property and return
+ // that instead. That way, if the component updates the property in SetParametersAsync, we won't revert it to the
+ // value we restored from the persistent state.
+ internal object? GetOrComputeLastValue()
+ {
+ var isInitialized = !ReferenceEquals(_lastValue, _uninitializedSentinel);
+ if (!isInitialized)
+ {
+ // Remove the uninitialized sentinel.
+ _lastValue = null;
+ if (_hasPendingInitialValue)
+ {
+ RestoreProperty();
+ _hasPendingInitialValue = false;
+ }
+ }
+ else
+ {
+ if (_ignoreComponentPropertyValue)
+ {
+ // At this point, we just received a value update from `RestoreProperty`.
+ // The property value might have been modified by the component and in this
+ // case we want to overwrite it with the value we just restored.
+ _ignoreComponentPropertyValue = false;
+ return _lastValue;
+ }
+ else
+ {
+ // In this case, the component might have modified the property value after
+ // we restored it from the persistent state. We don't want to overwrite it
+ // with a previously restored value.
+ var currentPropertyValue = _propertyGetter.GetValue(_subscriber.Component);
+ return currentPropertyValue;
+ }
+ }
+
+ return _lastValue;
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
+ [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")]
+ [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
+ [UnconditionalSuppressMessage("Trimming", "IL2077:'type' argument does not satisfy 'DynamicallyAccessedMemberTypes' in call to target method. The source field does not have matching annotations.", Justification = "Property types on components are preserved through other means.")]
+ internal void RestoreProperty()
+ {
+ var skipNotifications = _hasPendingInitialValue;
+ if (ReferenceEquals(_lastValue, _uninitializedSentinel) && !_hasPendingInitialValue)
+ {
+ // Upon subscribing, the callback might be invoked right away,
+ // but this is too early to restore the first value since the component state
+ // hasn't been fully initialized yet.
+ // For that reason, we make a mark to restore the state on GetOrComputeLastValue.
+ _hasPendingInitialValue = true;
+ return;
+ }
+
+ // The key needs to be computed here, do not move this outside of the lambda.
+ _storageKey ??= PersistentStateValueProviderKeyResolver.ComputeKey(_subscriber, _propertyName);
+
+ if (_customSerializer != null)
+ {
+ if (_state.TryTakeBytes(_storageKey, out var data))
+ {
+ Log.RestoringValueFromState(_logger, _storageKey, _propertyType.Name, _propertyName);
+ var sequence = new ReadOnlySequence(data!);
+ _lastValue = _customSerializer.Restore(_propertyType, sequence);
+ if (!skipNotifications)
+ {
+ _ignoreComponentPropertyValue = true;
+ _subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
+ }
+ }
+ else
+ {
+ Log.ValueNotFoundInPersistentState(_logger, _storageKey, _propertyType.Name, "null", _propertyName);
+ }
+ }
+ else
+ {
+ if (_state.TryTakeFromJson(_storageKey, _propertyType, out var value))
+ {
+ Log.RestoredValueFromPersistentState(_logger, _storageKey, _propertyType.Name, "null", _propertyName);
+ _lastValue = value;
+ if (!skipNotifications)
+ {
+ _ignoreComponentPropertyValue = true;
+ _subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
+ }
+ }
+ else
+ {
+ Log.NoValueToRestoreFromState(_logger, _storageKey, _propertyType.Name, _propertyName);
+ }
+ }
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
+ [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")]
+ [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
+ [UnconditionalSuppressMessage("Trimming", "IL2077:'type' argument does not satisfy 'DynamicallyAccessedMemberTypes' in call to target method. The source field does not have matching annotations.", Justification = "Property types on components are preserved through other means.")]
+ private Task PersistProperty()
+ {
+ // The key needs to be computed here, do not move this outside of the lambda.
+ _storageKey ??= PersistentStateValueProviderKeyResolver.ComputeKey(_subscriber, _propertyName);
+
+ var property = _propertyGetter.GetValue(_subscriber.Component);
+ if (property == null)
+ {
+ Log.SkippedPersistingNullValue(_logger, _storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
+ return Task.CompletedTask;
+ }
+
+ if (_customSerializer != null)
+ {
+ Log.PersistingValueToState(_logger, _storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
+
+ using var writer = new PooledArrayBufferWriter();
+ _customSerializer.Persist(_propertyType, property, writer);
+ _state.PersistAsBytes(_storageKey, writer.WrittenMemory.ToArray());
+ return Task.CompletedTask;
+ }
+
+ // Fallback to JSON serialization
+ Log.PersistingValueToState(_logger, _storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
+ _state.PersistAsJson(_storageKey, property, _propertyType);
+ return Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ _persistingSubscription?.Dispose();
+ _restoringSubscription?.Dispose();
+ }
+
+ private IPersistentComponentStateSerializer? SerializerFactory(Type type, IServiceProvider serviceProvider)
+ {
+ var serializerType = typeof(PersistentComponentStateSerializer<>).MakeGenericType(type);
+ var serializer = serviceProvider.GetService(serializerType);
+
+ // The generic class now inherits from the internal interface, so we can cast directly
+ return serializer as IPersistentComponentStateSerializer;
+ }
+
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2077:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations.",
+ Justification = "Properties of rendered components are preserved through other means and won't get trimmed.")]
+
+ private static PropertyGetter PropertyGetterFactory((Type type, string propertyName) key)
+ {
+ var (type, propertyName) = key;
+ var propertyInfo = GetPropertyInfo(type, propertyName);
+ if (propertyInfo == null || propertyInfo.GetMethod == null || !propertyInfo.GetMethod.IsPublic)
+ {
+ throw new InvalidOperationException(
+ $"A public property '{propertyName}' on component type '{type.FullName}' with a public getter wasn't found.");
+ }
+
+ return new PropertyGetter(type, propertyInfo);
+
+ static PropertyInfo? GetPropertyInfo([DynamicallyAccessedMembers(LinkerFlags.Component)] Type type, string propertyName)
+ => type.GetProperty(propertyName);
+ }
+
+ private static partial class Log
+ {
+ [LoggerMessage(1, LogLevel.Debug, "Persisting value for storage key '{StorageKey}' of type '{PropertyType}' from component '{ComponentType}' for property '{PropertyName}'", EventName = "PersistingValueToState")]
+ public static partial void PersistingValueToState(ILogger logger, string storageKey, string propertyType, string componentType, string propertyName);
+
+ [LoggerMessage(2, LogLevel.Debug, "Skipped persisting null value for storage key '{StorageKey}' of type '{PropertyType}' from component '{ComponentType}' for property '{PropertyName}'", EventName = "SkippedPersistingNullValue")]
+ public static partial void SkippedPersistingNullValue(ILogger logger, string storageKey, string propertyType, string componentType, string propertyName);
+
+ [LoggerMessage(3, LogLevel.Debug, "Restoring value for storage key '{StorageKey}' of type '{PropertyType}' for property '{PropertyName}'", EventName = "RestoringValueFromState")]
+ public static partial void RestoringValueFromState(ILogger logger, string storageKey, string propertyType, string propertyName);
+
+ [LoggerMessage(4, LogLevel.Debug, "No value to restore for storage key '{StorageKey}' of type '{PropertyType}' for property '{PropertyName}'", EventName = "NoValueToRestoreFromState")]
+ public static partial void NoValueToRestoreFromState(ILogger logger, string storageKey, string propertyType, string propertyName);
+
+ [LoggerMessage(5, LogLevel.Debug, "Restored value from persistent state for storage key '{StorageKey}' of type '{PropertyType}' for component '{ComponentType}' for property '{PropertyName}'", EventName = "RestoredValueFromPersistentState")]
+ public static partial void RestoredValueFromPersistentState(ILogger logger, string storageKey, string propertyType, string componentType, string propertyName);
+
+ [LoggerMessage(6, LogLevel.Debug, "Value not found in persistent state for storage key '{StorageKey}' of type '{PropertyType}' for component '{ComponentType}' for property '{PropertyName}'", EventName = "ValueNotFoundInPersistentState")]
+ public static partial void ValueNotFoundInPersistentState(ILogger logger, string storageKey, string propertyType, string componentType, string propertyName);
+ }
+}
diff --git a/src/Components/Components/src/PersistingComponentStateSubscription.cs b/src/Components/Components/src/PersistentState/PersistingComponentStateSubscription.cs
similarity index 100%
rename from src/Components/Components/src/PersistingComponentStateSubscription.cs
rename to src/Components/Components/src/PersistentState/PersistingComponentStateSubscription.cs
diff --git a/src/Components/Components/src/PersistentState/RestoreBehavior.cs b/src/Components/Components/src/PersistentState/RestoreBehavior.cs
new file mode 100644
index 000000000000..e958eed03c15
--- /dev/null
+++ b/src/Components/Components/src/PersistentState/RestoreBehavior.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;
+
+///
+/// Indicates the behavior to use when restoring state for a component parameter.
+///
+///
+/// By default, it always restores the value in all situations.
+/// Use to skip restoring the initial value
+/// when the host starts up.
+/// Use to skip restoring the last value captured
+/// the last time the current host was shut down, if the host supports restarting.
+///
+[Flags]
+public enum RestoreBehavior
+{
+ ///
+ /// Restore the value in all situations.
+ ///
+ Default = 0,
+ ///
+ /// Avoid restoring the initial value when the host starts up.
+ ///
+ SkipInitialValue = 1,
+
+ ///
+ /// Avoid restoring the last value captured when the current host was shut down.
+ ///
+ SkipLastSnapshot = 2
+}
diff --git a/src/Components/Components/src/PersistentState/RestoreComponentStateRegistration.cs b/src/Components/Components/src/PersistentState/RestoreComponentStateRegistration.cs
new file mode 100644
index 000000000000..1d5b16f8fce4
--- /dev/null
+++ b/src/Components/Components/src/PersistentState/RestoreComponentStateRegistration.cs
@@ -0,0 +1,9 @@
+// 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;
+
+internal readonly struct RestoreComponentStateRegistration(Action callback)
+{
+ public Action Callback { get; } = callback;
+}
diff --git a/src/Components/Components/src/PersistentState/RestoreContext.cs b/src/Components/Components/src/PersistentState/RestoreContext.cs
new file mode 100644
index 000000000000..461792bff6f7
--- /dev/null
+++ b/src/Components/Components/src/PersistentState/RestoreContext.cs
@@ -0,0 +1,58 @@
+// 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;
+
+///
+/// The context where the restore operation is taking place.
+///
+public sealed class RestoreContext
+{
+ private readonly bool _initialValue;
+ private readonly bool _lastSnapshot;
+ private readonly bool _allowUpdates;
+
+ ///
+ /// Gets a that indicates the host is restoring initial values.
+ ///
+ public static RestoreContext InitialValue { get; } = new RestoreContext(true, false, false);
+
+ ///
+ /// Gets a that indicates the host is restoring the last snapshot
+ /// available from the previous time the host was running.
+ ///
+ public static RestoreContext LastSnapshot { get; } = new RestoreContext(false, true, false);
+
+ ///
+ /// Gets the that indicates the host is providing an external state
+ /// update to the current value.
+ ///
+ public static RestoreContext ValueUpdate { get; } = new RestoreContext(false, false, true);
+
+ private RestoreContext(bool initialValue, bool lastSnapshot, bool allowUpdates)
+ {
+ _initialValue = initialValue;
+ _lastSnapshot = lastSnapshot;
+ _allowUpdates = allowUpdates;
+ }
+
+ internal bool ShouldRestore(RestoreOptions options)
+ {
+ if (_initialValue && !options.RestoreBehavior.HasFlag(RestoreBehavior.SkipInitialValue))
+ {
+ return true;
+ }
+
+ if (_lastSnapshot && !options.RestoreBehavior.HasFlag(RestoreBehavior.SkipLastSnapshot))
+ {
+ return true;
+ }
+
+ if (_allowUpdates && options.AllowUpdates)
+ {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/Components/Components/src/PersistentState/RestoreOptions.cs b/src/Components/Components/src/PersistentState/RestoreOptions.cs
new file mode 100644
index 000000000000..4f2696f6546f
--- /dev/null
+++ b/src/Components/Components/src/PersistentState/RestoreOptions.cs
@@ -0,0 +1,28 @@
+// 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 the options available for a restore operation.
+///
+public readonly struct RestoreOptions
+{
+ ///
+ /// Initializes a new instance of the struct with default values.
+ ///
+ public RestoreOptions()
+ {
+ }
+
+ ///
+ /// Gets the behavior to use when restoring data.
+ ///
+ public RestoreBehavior RestoreBehavior { get; init; } = RestoreBehavior.Default;
+
+ ///
+ /// Gets a value indicating whether the registration wants to receive updates beyond
+ /// the initially provided value.
+ ///
+ public bool AllowUpdates { get; init; } = false;
+}
diff --git a/src/Components/Components/src/PersistentState/RestoringComponentStateSubscription.cs b/src/Components/Components/src/PersistentState/RestoringComponentStateSubscription.cs
new file mode 100644
index 000000000000..48b2319ba7cf
--- /dev/null
+++ b/src/Components/Components/src/PersistentState/RestoringComponentStateSubscription.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;
+
+///
+/// Represents a subscription to component state restoration events. Dispose to unsubscribe.
+///
+public readonly struct RestoringComponentStateSubscription : IDisposable
+{
+ private readonly List _registeredRestoringCallbacks;
+ private readonly RestoreComponentStateRegistration? _registration;
+
+ internal RestoringComponentStateSubscription(
+ List registeredRestoringCallbacks,
+ RestoreComponentStateRegistration registration)
+ {
+ _registeredRestoringCallbacks = registeredRestoringCallbacks;
+ _registration = registration;
+ }
+
+ ///
+ /// Unsubscribes from state restoration events.
+ ///
+ public void Dispose()
+ {
+ if (_registration.HasValue)
+ {
+ _registeredRestoringCallbacks?.Remove(_registration.Value);
+ }
+ }
+}
diff --git a/src/Components/Components/src/PersistentStateAttribute.cs b/src/Components/Components/src/PersistentStateAttribute.cs
index 79fd4f9ac838..cd8de101bda9 100644
--- a/src/Components/Components/src/PersistentStateAttribute.cs
+++ b/src/Components/Components/src/PersistentStateAttribute.cs
@@ -10,4 +10,21 @@ namespace Microsoft.AspNetCore.Components;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class PersistentStateAttribute : CascadingParameterAttributeBase
{
+ ///
+ /// Gets or sets the behavior to use when restoring data.
+ ///
+ ///
+ /// By default it always restores the value on all situations.
+ /// Use
to skip restoring the initial value
+ /// when the host starts up.
+ /// Use
to skip restoring the last value captured
+ /// the last time the current host was shut down.
+ ///
+ public RestoreBehavior RestoreBehavior { get; set; } = RestoreBehavior.Default;
+
+ ///
+ /// Gets or sets a value whether the component wants to receive updates to the parameter
+ /// beyond the initial value provided during initialization.
+ ///
+ public bool AllowUpdates { get; set; }
}
diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs
deleted file mode 100644
index 18589473d4e3..000000000000
--- a/src/Components/Components/src/PersistentStateValueProvider.cs
+++ /dev/null
@@ -1,382 +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 System.Buffers;
-using System.Collections.Concurrent;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Reflection;
-using System.Security.Cryptography;
-using System.Text;
-using Microsoft.AspNetCore.Components.HotReload;
-using Microsoft.AspNetCore.Components.Reflection;
-using Microsoft.AspNetCore.Components.Rendering;
-using Microsoft.AspNetCore.Internal;
-
-namespace Microsoft.AspNetCore.Components.Infrastructure;
-
-internal sealed class PersistentStateValueProvider(PersistentComponentState state, IServiceProvider serviceProvider) : ICascadingValueSupplier
-{
- private static readonly ConcurrentDictionary<(string, string, string), byte[]> _keyCache = new();
- private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new();
- private static readonly ConcurrentDictionary _serializerCache = new();
-
- static PersistentStateValueProvider()
- {
- if (HotReloadManager.Default.MetadataUpdateSupported)
- {
- HotReloadManager.Default.OnDeltaApplied += ClearCaches;
- }
- }
-
- private static void ClearCaches()
- {
- _propertyGetterCache.Clear();
- _serializerCache.Clear();
- }
-
- private readonly Dictionary _subscriptions = [];
-
- public bool IsFixed => false;
- // For testing purposes only
- internal Dictionary Subscriptions => _subscriptions;
-
- public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
- => parameterInfo.Attribute is PersistentStateAttribute;
-
- [UnconditionalSuppressMessage(
- "ReflectionAnalysis",
- "IL2026:RequiresUnreferencedCode message",
- Justification = "JSON serialization and deserialization might require types that cannot be statically analyzed.")]
- [UnconditionalSuppressMessage(
- "Trimming",
- "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
- Justification = "JSON serialization and deserialization might require types that cannot be statically analyzed.")]
- public object? GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo)
- {
- var componentState = (ComponentState)key!;
- var storageKey = ComputeKey(componentState, parameterInfo.PropertyName);
-
- // Try to get a custom serializer for this type first
- var customSerializer = _serializerCache.GetOrAdd(parameterInfo.PropertyType, SerializerFactory);
-
- if (customSerializer != null)
- {
- if (state.TryTakeBytes(storageKey, out var data))
- {
- var sequence = new ReadOnlySequence(data!);
- return customSerializer.Restore(parameterInfo.PropertyType, sequence);
- }
- return null;
- }
-
- // Fallback to JSON serialization
- return state.TryTakeFromJson(storageKey, parameterInfo.PropertyType, out var value) ? value : null;
- }
-
- [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
- [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")]
- [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
- public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
- {
- var propertyName = parameterInfo.PropertyName;
- var propertyType = parameterInfo.PropertyType;
-
- // Resolve serializer outside the lambda
- var customSerializer = _serializerCache.GetOrAdd(propertyType, SerializerFactory);
-
- _subscriptions[subscriber] = state.RegisterOnPersisting(() =>
- {
- var storageKey = ComputeKey(subscriber, propertyName);
- var propertyGetter = ResolvePropertyGetter(subscriber.Component.GetType(), propertyName);
- var property = propertyGetter.GetValue(subscriber.Component);
- if (property == null)
- {
- return Task.CompletedTask;
- }
-
- if (customSerializer != null)
- {
- using var writer = new PooledArrayBufferWriter();
- customSerializer.Persist(propertyType, property, writer);
- state.PersistAsBytes(storageKey, writer.WrittenMemory.ToArray());
- return Task.CompletedTask;
- }
-
- // Fallback to JSON serialization
- state.PersistAsJson(storageKey, property, propertyType);
- return Task.CompletedTask;
- }, subscriber.Renderer.GetComponentRenderMode(subscriber.Component));
- }
-
- private static PropertyGetter ResolvePropertyGetter(Type type, string propertyName)
- {
- return _propertyGetterCache.GetOrAdd((type, propertyName), PropertyGetterFactory);
- }
-
- private IPersistentComponentStateSerializer? SerializerFactory(Type type)
- {
- var serializerType = typeof(PersistentComponentStateSerializer<>).MakeGenericType(type);
- var serializer = serviceProvider.GetService(serializerType);
-
- // The generic class now inherits from the internal interface, so we can cast directly
- return serializer as IPersistentComponentStateSerializer;
- }
-
- [UnconditionalSuppressMessage(
- "Trimming",
- "IL2077:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations.",
- Justification = "Properties of rendered components are preserved through other means and won't get trimmed.")]
-
- private static PropertyGetter PropertyGetterFactory((Type type, string propertyName) key)
- {
- var (type, propertyName) = key;
- var propertyInfo = GetPropertyInfo(type, propertyName);
- if (propertyInfo == null)
- {
- throw new InvalidOperationException($"Property {propertyName} not found on type {type.FullName}");
- }
- return new PropertyGetter(type, propertyInfo);
-
- static PropertyInfo? GetPropertyInfo([DynamicallyAccessedMembers(LinkerFlags.Component)] Type type, string propertyName)
- => type.GetProperty(propertyName);
- }
-
- public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
- {
- if (_subscriptions.TryGetValue(subscriber, out var subscription))
- {
- subscription.Dispose();
- _subscriptions.Remove(subscriber);
- }
- }
-
- // Internal for testing only
- internal static string ComputeKey(ComponentState componentState, string propertyName)
- {
- // We need to come up with a pseudo-unique key for the storage key.
- // We need to consider the property name, the component type, and its position within the component tree.
- // If only one component of a given type is present on the page, then only the component type + property name is enough.
- // If multiple components of the same type are present on the page, then we need to consider the position within the tree.
- // To do that, we are going to use the `@key` directive on the component if present and if we deem it serializable.
- // Serializable keys are Guid, DateOnly, TimeOnly, and any primitive type.
- // The key is composed of four segments:
- // Parent component type
- // Component type
- // Property name
- // @key directive if present and serializable.
- // We combine the first three parts into an identifier, and then we generate a derived identifier with the key
- // We do it this way becasue the information for the first three pieces of data is static for the lifetime of the
- // program and can be cached on each situation.
-
- var parentComponentType = GetParentComponentType(componentState);
- var componentType = GetComponentType(componentState);
-
- var preKey = _keyCache.GetOrAdd((parentComponentType, componentType, propertyName), KeyFactory);
- var finalKey = ComputeFinalKey(preKey, componentState);
-
- return finalKey;
- }
-
- private static string ComputeFinalKey(byte[] preKey, ComponentState componentState)
- {
- Span keyHash = stackalloc byte[SHA256.HashSizeInBytes];
-
- var key = GetSerializableKey(componentState);
- byte[]? pool = null;
- try
- {
- Span keyBuffer = stackalloc byte[1024];
- var currentBuffer = keyBuffer;
- preKey.CopyTo(keyBuffer);
- if (key is IUtf8SpanFormattable spanFormattable)
- {
- var wroteKey = false;
- while (!wroteKey)
- {
- currentBuffer = keyBuffer[preKey.Length..];
- wroteKey = spanFormattable.TryFormat(currentBuffer, out var written, "", CultureInfo.InvariantCulture);
- if (!wroteKey)
- {
- // It is really unlikely that we will enter here, but we need to handle this case
- Debug.Assert(written == 0);
- GrowBuffer(ref pool, ref keyBuffer);
- }
- else
- {
- currentBuffer = currentBuffer[..written];
- }
- }
- }
- else
- {
- var keySpan = ResolveKeySpan(key);
- var wroteKey = false;
- while (!wroteKey)
- {
- currentBuffer = keyBuffer[preKey.Length..];
- wroteKey = Encoding.UTF8.TryGetBytes(keySpan, currentBuffer, out var written);
- if (!wroteKey)
- {
- // It is really unlikely that we will enter here, but we need to handle this case
- Debug.Assert(written == 0);
- // Since this is utf-8, grab a buffer the size of the key * 4 + the preKey size
- // this guarantees we have enough space to encode the key
- GrowBuffer(ref pool, ref keyBuffer, keySpan.Length * 4 + preKey.Length);
- }
- else
- {
- currentBuffer = currentBuffer[..written];
- }
- }
- }
-
- keyBuffer = keyBuffer[..(preKey.Length + currentBuffer.Length)];
-
- var hashSucceeded = SHA256.TryHashData(keyBuffer, keyHash, out _);
- Debug.Assert(hashSucceeded);
- return Convert.ToBase64String(keyHash);
- }
- finally
- {
- if (pool != null)
- {
- ArrayPool.Shared.Return(pool, clearArray: true);
- }
- }
- }
-
- private static ReadOnlySpan ResolveKeySpan(object? key)
- {
- if (key is IFormattable formattable)
- {
- var keyString = formattable.ToString("", CultureInfo.InvariantCulture);
- return keyString.AsSpan();
- }
- else if (key is IConvertible convertible)
- {
- var keyString = convertible.ToString(CultureInfo.InvariantCulture);
- return keyString.AsSpan();
- }
- return default;
- }
-
- private static void GrowBuffer(ref byte[]? pool, ref Span keyBuffer, int? size = null)
- {
- var newPool = pool == null ? ArrayPool.Shared.Rent(size ?? 2048) : ArrayPool.Shared.Rent(pool.Length * 2);
- keyBuffer.CopyTo(newPool);
- keyBuffer = newPool;
- if (pool != null)
- {
- ArrayPool.Shared.Return(pool, clearArray: true);
- }
- pool = newPool;
- }
-
- private static object? GetSerializableKey(ComponentState componentState)
- {
- var componentKey = componentState.GetComponentKey();
- if (componentKey != null && IsSerializableKey(componentKey))
- {
- return componentKey;
- }
-
- return null;
- }
-
- private static string GetComponentType(ComponentState componentState) => componentState.Component.GetType().FullName!;
-
- private static string GetParentComponentType(ComponentState componentState)
- {
- if (componentState.ParentComponentState == null)
- {
- return "";
- }
- if (componentState.ParentComponentState.Component == null)
- {
- return "";
- }
-
- if (componentState.ParentComponentState.ParentComponentState != null)
- {
- var renderer = componentState.Renderer;
- var parentRenderMode = renderer.GetComponentRenderMode(componentState.ParentComponentState.Component);
- var grandParentRenderMode = renderer.GetComponentRenderMode(componentState.ParentComponentState.ParentComponentState.Component);
- if (parentRenderMode != grandParentRenderMode)
- {
- // This is the case when EndpointHtmlRenderer introduces an SSRRenderBoundary component.
- // We want to return "" because the SSRRenderBoundary component is not a real component
- // and won't appear on the component tree in the WebAssemblyRenderer and RemoteRenderer
- // interactive scenarios.
- return "";
- }
- }
-
- return GetComponentType(componentState.ParentComponentState);
- }
-
- private static byte[] KeyFactory((string parentComponentType, string componentType, string propertyName) parts) =>
- SHA256.HashData(Encoding.UTF8.GetBytes(string.Join(".", parts.parentComponentType, parts.componentType, parts.propertyName)));
-
- private static bool IsSerializableKey(object key)
- {
- if (key == null)
- {
- return false;
- }
- var keyType = key.GetType();
- var result = Type.GetTypeCode(keyType) != TypeCode.Object
- || keyType == typeof(Guid)
- || keyType == typeof(DateTimeOffset)
- || keyType == typeof(DateOnly)
- || keyType == typeof(TimeOnly);
-
- return result;
- }
-
- ///
- /// Serializes using the provided and persists it under the given .
- ///
- /// The type.
- /// The key to use to persist the state.
- /// The instance to persist.
- /// The custom serializer to use for serialization.
- internal void PersistAsync(string key, TValue instance, PersistentComponentStateSerializer serializer)
- {
- ArgumentNullException.ThrowIfNull(key);
- ArgumentNullException.ThrowIfNull(serializer);
-
- using var writer = new PooledArrayBufferWriter();
- serializer.Persist(instance, writer);
- state.PersistAsBytes(key, writer.WrittenMemory.ToArray());
- }
-
- ///
- /// Tries to retrieve the persisted state with the given and deserializes it using the provided into an
- /// instance of type .
- /// When the key is present, the state is successfully returned via
- /// and removed from the .
- ///
- /// The key used to persist the instance.
- /// The custom serializer to use for deserialization.
- /// The persisted instance.
- /// true if the state was found; false otherwise.
- internal bool TryTake(string key, PersistentComponentStateSerializer serializer, [MaybeNullWhen(false)] out TValue instance)
- {
- ArgumentNullException.ThrowIfNull(key);
- ArgumentNullException.ThrowIfNull(serializer);
-
- if (state.TryTakeBytes(key, out var data))
- {
- var sequence = new ReadOnlySequence(data!);
- instance = serializer.Restore(sequence);
- return true;
- }
- else
- {
- instance = default;
- return false;
- }
- }
-}
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 442ad26d73a2..2da7d492e710 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -1,6 +1,27 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList? properties) -> void
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.RestoreStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.RestoreContext! context) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Components.PersistentComponentState.RegisterOnRestoring(System.Action! callback, Microsoft.AspNetCore.Components.RestoreOptions options) -> Microsoft.AspNetCore.Components.RestoringComponentStateSubscription
+Microsoft.AspNetCore.Components.PersistentStateAttribute.AllowUpdates.get -> bool
+Microsoft.AspNetCore.Components.PersistentStateAttribute.AllowUpdates.set -> void
+Microsoft.AspNetCore.Components.PersistentStateAttribute.RestoreBehavior.get -> Microsoft.AspNetCore.Components.RestoreBehavior
+Microsoft.AspNetCore.Components.PersistentStateAttribute.RestoreBehavior.set -> void
+Microsoft.AspNetCore.Components.Rendering.ComponentState.Renderer.get -> Microsoft.AspNetCore.Components.RenderTree.Renderer!
Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList? properties = null) -> void
+Microsoft.AspNetCore.Components.RestoreBehavior
+Microsoft.AspNetCore.Components.RestoreBehavior.Default = 0 -> Microsoft.AspNetCore.Components.RestoreBehavior
+Microsoft.AspNetCore.Components.RestoreBehavior.SkipInitialValue = 1 -> Microsoft.AspNetCore.Components.RestoreBehavior
+Microsoft.AspNetCore.Components.RestoreBehavior.SkipLastSnapshot = 2 -> Microsoft.AspNetCore.Components.RestoreBehavior
+Microsoft.AspNetCore.Components.RestoreContext
+Microsoft.AspNetCore.Components.RestoreOptions
+Microsoft.AspNetCore.Components.RestoreOptions.AllowUpdates.get -> bool
+Microsoft.AspNetCore.Components.RestoreOptions.AllowUpdates.init -> void
+Microsoft.AspNetCore.Components.RestoreOptions.RestoreBehavior.get -> Microsoft.AspNetCore.Components.RestoreBehavior
+Microsoft.AspNetCore.Components.RestoreOptions.RestoreBehavior.init -> void
+Microsoft.AspNetCore.Components.RestoreOptions.RestoreOptions() -> void
+Microsoft.AspNetCore.Components.RestoringComponentStateSubscription
+Microsoft.AspNetCore.Components.RestoringComponentStateSubscription.Dispose() -> void
+Microsoft.AspNetCore.Components.RestoringComponentStateSubscription.RestoringComponentStateSubscription() -> void
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type?
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.set -> void
Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions
@@ -24,6 +45,9 @@ abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.R
static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+static Microsoft.AspNetCore.Components.RestoreContext.InitialValue.get -> Microsoft.AspNetCore.Components.RestoreContext!
+static Microsoft.AspNetCore.Components.RestoreContext.LastSnapshot.get -> Microsoft.AspNetCore.Components.RestoreContext!
+static Microsoft.AspNetCore.Components.RestoreContext.ValueUpdate.get -> Microsoft.AspNetCore.Components.RestoreContext!
virtual Microsoft.AspNetCore.Components.OwningComponentBase.DisposeAsyncCore() -> System.Threading.Tasks.ValueTask
static Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object?
diff --git a/src/Components/Components/src/Reflection/PropertyGetter.cs b/src/Components/Components/src/Reflection/PropertyGetter.cs
index 03fa596cbc5c..c458feda0af7 100644
--- a/src/Components/Components/src/Reflection/PropertyGetter.cs
+++ b/src/Components/Components/src/Reflection/PropertyGetter.cs
@@ -14,12 +14,16 @@ internal sealed class PropertyGetter
private readonly Func