Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ee805dc
Unquarantine InteropTests.cs (#62817)
BrennanConroy Jul 19, 2025
0371d87
Obsolete Microsoft.AspNetCore.HttpOverrides.IPNetwork (#62490)
WeihanLi Jul 19, 2025
57d1093
Update dependencies from https://github.com/dotnet/dotnet build 27581…
dotnet-maestro[bot] Jul 19, 2025
b9f43c1
Fix memory pool metrics unit to follow standard (#62766)
JamesNK Jul 19, 2025
1f0c98b
Identity metrics clean up (#62671)
JamesNK Jul 19, 2025
7d7e819
Update dependencies from https://github.com/dotnet/dotnet build 27589…
dotnet-maestro[bot] Jul 20, 2025
b568259
Support all subscribers to `OnNotFound` event (#62798)
ilonatommy Jul 21, 2025
0881ca3
Implement scenario-based persistent state filtering for Blazor
javiercn Jul 4, 2025
1caa356
Reconnection
javiercn Jul 5, 2025
170196f
tmp
javiercn Jul 11, 2025
60b83dc
Fix build
javiercn Jul 13, 2025
de009b0
Fix tests
javiercn Jul 14, 2025
ea5a063
Make it work for reconnection
javiercn Jul 14, 2025
4979ff9
Fix tests
javiercn Jul 14, 2025
b4658af
Remove plan
javiercn Jul 14, 2025
89bd247
Cleanup
javiercn Jul 14, 2025
a3870f5
Review
javiercn Jul 15, 2025
8469199
Prerendering and resume working, only enhanced navigation left
javiercn Jul 15, 2025
a2d19fb
Push updated state
javiercn Jul 15, 2025
4869471
Webassembly working (I believe)
javiercn Jul 15, 2025
b182822
Tmp
javiercn Jul 16, 2025
8cc5391
Fix build
javiercn Jul 16, 2025
c2dbf37
Undo samples
javiercn Jul 16, 2025
299992e
Undo instructions
javiercn Jul 16, 2025
863316a
Do not update properties when updated after restoring
javiercn Jul 16, 2025
3255c06
tmp
javiercn Jul 16, 2025
2400680
Revert "Undo samples"
javiercn Jul 16, 2025
522b386
Updated sample
javiercn Jul 16, 2025
4cf73ea
tmp
javiercn Jul 17, 2025
098c691
tmp
javiercn Jul 18, 2025
f74682b
Provide a key as it's required for enhanced nav to work
javiercn Jul 18, 2025
e776dfe
Tests working
javiercn Jul 18, 2025
7bdfbf3
Refactor
javiercn Jul 19, 2025
3be9678
Fixing the build
javiercn Jul 19, 2025
4c41899
Fixing the build
javiercn Jul 19, 2025
1c89791
tmp
javiercn Jul 19, 2025
af5ef21
Move persistent state types to the PersistentState folder
javiercn Jul 19, 2025
9395fee
tmp
javiercn Jul 19, 2025
99005d3
Cleanup tests
javiercn Jul 19, 2025
1110186
Update tests
javiercn Jul 19, 2025
c0dfb54
Fix tests
javiercn Jul 19, 2025
f3301ba
Tests working
javiercn Jul 19, 2025
ae94c3c
Remove unwanted changes
javiercn Jul 19, 2025
bfdf7a7
Cleanup
javiercn Jul 19, 2025
31654d2
Undo sample changes
javiercn Jul 19, 2025
87b5b04
More cleanups
javiercn Jul 19, 2025
1717a7f
More cleanups
javiercn Jul 19, 2025
e412cc2
Implement filtering for persistent services
javiercn Jul 19, 2025
5d30685
Update E2E test
javiercn Jul 19, 2025
f33f2c0
Fix tests
javiercn Jul 19, 2025
8e0a0ad
Fix build
javiercn Jul 19, 2025
6568966
tmp
javiercn Jul 20, 2025
bcea924
Ensure persisted component state gets passed at the end of streaming …
javiercn Jul 20, 2025
2e70c08
Fix keyless enhanced nav updates
javiercn Jul 20, 2025
90d25f9
Fix build
javiercn Jul 20, 2025
4bc47f9
Remove unwanted sample changes
javiercn Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
366 changes: 183 additions & 183 deletions eng/Version.Details.xml

Large diffs are not rendered by default.

176 changes: 88 additions & 88 deletions eng/Versions.props

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion eng/common/tools.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements =

# Locate Visual Studio installation or download x-copy msbuild.
$vsInfo = LocateVisualStudio $vsRequirements
if ($vsInfo -ne $null) {
if ($vsInfo -ne $null -and $env:ForceUseXCopyMSBuild -eq $null) {
# Ensure vsInstallDir has a trailing slash
$vsInstallDir = Join-Path $vsInfo.installationPath "\"
$vsMajorVersion = $vsInfo.installationVersion.Split('.')[0]
Expand Down
6 changes: 3 additions & 3 deletions global.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
"jdk": "latest"
},
"msbuild-sdks": {
"Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25367.101",
"Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.25367.101",
"Microsoft.DotNet.SharedFramework.Sdk": "10.0.0-beta.25367.101",
"Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25368.105",
"Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.25368.105",
"Microsoft.DotNet.SharedFramework.Sdk": "10.0.0-beta.25368.105",
"Microsoft.Build.NoTargets": "3.7.0",
"Microsoft.Build.Traversal": "3.4.0"
}
Expand Down
22 changes: 22 additions & 0 deletions src/Components/Components/src/ComponentSubscriptionKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.Infrastructure;

internal struct ComponentSubscriptionKey(ComponentState subscriber, string propertyName) : IEquatable<ComponentSubscriptionKey>
{
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ internal interface IPersistentComponentStateSerializer
{
void Persist(Type type, object value, IBufferWriter<byte> writer);
object Restore(Type type, ReadOnlySequence<byte> data);
}
}
50 changes: 47 additions & 3 deletions src/Components/Components/src/PersistentComponentState.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,24 +17,30 @@ public class PersistentComponentState
private readonly IDictionary<string, byte[]> _currentState;

private readonly List<PersistComponentStateRegistration> _registeredCallbacks;
private readonly List<RestoreComponentStateRegistration> _registeredRestoringCallbacks;

internal PersistentComponentState(
IDictionary<string , byte[]> currentState,
List<PersistComponentStateRegistration> pauseCallbacks)
IDictionary<string, byte[]> currentState,
List<PersistComponentStateRegistration> pauseCallbacks,
List<RestoreComponentStateRegistration> restoringCallbacks)
{
_currentState = currentState;
_registeredCallbacks = pauseCallbacks;
_registeredRestoringCallbacks = restoringCallbacks;
}

internal bool PersistingState { get; set; }

internal void InitializeExistingState(IDictionary<string, byte[]> existingState)
internal RestoreContext CurrentContext { get; private set; } = RestoreContext.InitialValue;

internal void InitializeExistingState(IDictionary<string, byte[]> existingState, RestoreContext context)
{
if (_existingState != null)
{
throw new InvalidOperationException("PersistentComponentState already initialized.");
}
_existingState = existingState ?? throw new ArgumentNullException(nameof(existingState));
CurrentContext = context;
}

/// <summary>
Expand Down Expand Up @@ -68,6 +75,30 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
return new PersistingComponentStateSubscription(_registeredCallbacks, persistenceCallback);
}

/// <summary>
/// Register a callback to restore the state when the application state is being restored.
/// </summary>
/// <param name="callback"> The callback to invoke when the application state is being restored.</param>
/// <param name="options">Options that control the restoration behavior.</param>
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
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;
}

/// <summary>
/// Serializes <paramref name="instance"/> as JSON and persists it under the given <paramref name="key"/>.
/// </summary>
Expand Down Expand Up @@ -214,4 +245,17 @@ private bool TryTake(string key, out byte[]? value)
return false;
}
}

internal void UpdateExistingState(IDictionary<string, byte[]> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ namespace Microsoft.AspNetCore.Components.Infrastructure;
public class ComponentStatePersistenceManager
{
private readonly List<PersistComponentStateRegistration> _registeredCallbacks = new();
private readonly List<RestoreComponentStateRegistration> _registeredRestoringCallbacks = new();
private readonly ILogger<ComponentStatePersistenceManager> _logger;

private bool _stateIsPersisted;
private bool _stateIsInitialized;
private readonly PersistentServicesRegistry? _servicesRegistry;
private readonly Dictionary<string, byte[]> _currentState = new(StringComparer.Ordinal);

Expand All @@ -24,7 +26,7 @@ public class ComponentStatePersistenceManager
/// <param name="logger"></param>
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
{
State = new PersistentComponentState(_currentState, _registeredCallbacks);
State = new PersistentComponentState(_currentState, _registeredCallbacks, _registeredRestoringCallbacks);
_logger = logger;
}

Expand Down Expand Up @@ -55,10 +57,39 @@ public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
public async Task RestoreStateAsync(IPersistentComponentStateStore store)
{
await RestoreStateAsync(store, RestoreContext.InitialValue);
}

/// <summary>
/// Restores the application state.
/// </summary>
/// <param name="store"> The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
/// <param name="context">The <see cref="RestoreContext"/> that provides additional context for the restoration.</param>
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
public async Task RestoreStateAsync(IPersistentComponentStateStore store, RestoreContext context)
{
var data = await store.GetPersistedStateAsync();
State.InitializeExistingState(data);
_servicesRegistry?.Restore(State);

if (_stateIsInitialized)
{
State.UpdateExistingState(data, context);
}
else
{
State.InitializeExistingState(data, context);
_servicesRegistry?.RegisterForPersistence(State);
_stateIsInitialized = true;
}

if (context == RestoreContext.ValueUpdate)
{
foreach (var registration in _registeredRestoringCallbacks)
{
registration.Callback();
}
return;
}
}

/// <summary>
Expand All @@ -78,9 +109,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<IPersistentComponentStateStore> compositeStore)
Expand Down Expand Up @@ -271,4 +299,5 @@ static async Task<bool> AnyTaskFailed(List<Task<bool>> pendingCallbackTasks)
return true;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,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<PersistingComponentStateSubscription> _subscriptions = [];
private List<(PersistingComponentStateSubscription, RestoringComponentStateSubscription)> _subscriptions = [];
private static readonly ConcurrentDictionary<Type, PropertiesAccessor> _cachedAccessorsByType = new();

public PersistentServicesRegistry(IServiceProvider serviceProvider)
Expand All @@ -45,7 +45,9 @@ internal void RegisterForPersistence(PersistentComponentState state)
return;
}

var subscriptions = new List<PersistingComponentStateSubscription>(_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];
Expand All @@ -58,20 +60,32 @@ 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),
state.RegisterOnRestoring(() =>
{
RestoreRegistrationsIfAvailable(state);
}, new RestoreOptions { RestoreBehavior = RestoreBehavior.SkipLastSnapshot })));
}

_subscriptions = subscriptions;
Expand All @@ -83,7 +97,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)
{
Expand All @@ -96,14 +110,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<PersistentServiceRegistration[]>(_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.")]
Expand Down Expand Up @@ -131,9 +143,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!);
}
}
Expand All @@ -156,12 +172,12 @@ private sealed class PropertiesAccessor
{
internal const BindingFlags BindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;

private readonly Dictionary<string, (PropertySetter, PropertyGetter)> _underlyingAccessors;
private readonly Dictionary<string, (PropertySetter, PropertyGetter, RestoreOptions)> _underlyingAccessors;
private readonly (string, Type)[] _cachedKeysForService;

public PropertiesAccessor([DynamicallyAccessedMembers(LinkerFlags.Component)] Type targetType, Type keyType)
{
_underlyingAccessors = new Dictionary<string, (PropertySetter, PropertyGetter)>(StringComparer.OrdinalIgnoreCase);
_underlyingAccessors = new Dictionary<string, (PropertySetter, PropertyGetter, RestoreOptions)>(StringComparer.OrdinalIgnoreCase);

var keys = new List<(string, Type)>();
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
Expand Down Expand Up @@ -195,10 +211,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];
Expand Down Expand Up @@ -227,7 +249,7 @@ internal static IEnumerable<PropertyInfo> 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;
}

Expand Down
Loading
Loading