From c67cbd016056f65f2b4454816a80cd6dd6de1235 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 19 May 2025 13:55:27 +0200 Subject: [PATCH 1/3] WIP --- .../PersistentServicesRegistry.cs | 17 +++++++- .../src/AuthenticationStateSerializer.cs | 39 ++++++------------- ...ssemblyRazorComponentsBuilderExtensions.cs | 4 ++ ...DeserializedAuthenticationStateProvider.cs | 35 ++++++++--------- ...thenticationServiceCollectionExtensions.cs | 7 +++- 5 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs b/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs index 3a91043bb6d9..5ba38df41c10 100644 --- a/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs +++ b/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs @@ -89,6 +89,7 @@ private static void PersistInstanceState(object instance, Type type, PersistentC { state.PersistAsJson(key, value, propertyType); } + Console.WriteLine($"[Persist] type: {instance.GetType()}, propertyType: {propertyType}, key: {key}, result: {value}"); } } @@ -136,6 +137,7 @@ private static void RestoreInstanceState(object instance, Type type, PersistentC var (setter, getter) = accessors.GetAccessor(key); setter.SetValue(instance, result!); } + Console.WriteLine($"[Restore] type: {instance.GetType()}, propertyType: {propertyType}, key: {key}, result: {result}"); } } @@ -211,8 +213,19 @@ private static string ComputeKey(Type keyType, string propertyName) // This happens once per type and property combo, so allocations are ok. var assemblyName = keyType.Assembly.FullName; var typeName = keyType.FullName; - var input = Encoding.UTF8.GetBytes(string.Join(".", assemblyName, typeName, propertyName)); - return Convert.ToBase64String(SHA256.HashData(input)); + + // Internal classes can be bundled in different assemblies during prerendering and WASM rendering. + bool isTypeInternal = (!keyType.IsPublic && !keyType.IsNested) || keyType.IsNestedAssembly; + var inputString = isTypeInternal + ? string.Join(".", typeName, propertyName) + : string.Join(".", assemblyName, typeName, propertyName); + + var input = Encoding.UTF8.GetBytes(inputString); + var hash = SHA256.HashData(input); + var key = Convert.ToBase64String(hash); + + Console.WriteLine($"[ComputeKey] inputString: {inputString}, key: {key}"); + return key; } internal static IEnumerable GetCandidateBindableProperties( diff --git a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs index 4c4a83e23196..b1f5488bc074 100644 --- a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs +++ b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs @@ -2,51 +2,34 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Components.WebAssembly.Server; -internal sealed class AuthenticationStateSerializer : IHostEnvironmentAuthenticationStateProvider, IDisposable +internal sealed class AuthenticationStateSerializer : IHostEnvironmentAuthenticationStateProvider { - // Do not change. This must match all versions of the server-side DeserializedAuthenticationStateProvider.PersistenceKey. - internal const string PersistenceKey = $"__internal__{nameof(AuthenticationState)}"; - - private readonly PersistentComponentState _state; private readonly Func> _serializeCallback; - private readonly PersistingComponentStateSubscription _subscription; - private Task? _authenticationStateTask; + [SupplyParameterFromPersistentComponentState] + public AuthenticationStateData? CurrentAuthenticationState { get; set; } - public AuthenticationStateSerializer(PersistentComponentState persistentComponentState, IOptions options) + public AuthenticationStateSerializer(IOptions options) { - _state = persistentComponentState; _serializeCallback = options.Value.SerializationCallback; - _subscription = persistentComponentState.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); - } - - private async Task OnPersistingAsync() - { - if (_authenticationStateTask is null) - { - throw new InvalidOperationException($"{nameof(SetAuthenticationState)} must be called before the {nameof(PersistentComponentState)}.{nameof(PersistentComponentState.RegisterOnPersisting)} callback."); - } - - var authenticationStateData = await _serializeCallback(await _authenticationStateTask); - if (authenticationStateData is not null) - { - _state.PersistAsJson(PersistenceKey, authenticationStateData); - } } /// public void SetAuthenticationState(Task authenticationStateTask) { - _authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask)); + ArgumentNullException.ThrowIfNull(authenticationStateTask, nameof(authenticationStateTask)); + + // fire and forget, not good... This method can throw, especially on serialization. + _ = SetAuthenticationStateAsync(authenticationStateTask); } - public void Dispose() + private async Task SetAuthenticationStateAsync(Task authenticationStateTask) { - _subscription.Dispose(); + var authenticationState = await authenticationStateTask; + CurrentAuthenticationState = await _serializeCallback(authenticationState); } } diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs index d783bede44f5..9d270ad25254 100644 --- a/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs +++ b/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs @@ -4,7 +4,9 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; +using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Server; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -41,6 +43,8 @@ public static IRazorComponentsBuilder AddInteractiveWebAssemblyComponents(this I public static IRazorComponentsBuilder AddAuthenticationStateSerialization(this IRazorComponentsBuilder builder, Action? configure = null) { builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped()); + builder.Services.TryAddScoped(sp => (AuthenticationStateSerializer)sp.GetRequiredService()); + RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(builder.Services, RenderMode.InteractiveAuto); if (configure is not null) { builder.Services.Configure(configure); diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs index bff25cd51fbe..a51240e62f72 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs @@ -1,40 +1,37 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Options; -using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication; internal sealed class DeserializedAuthenticationStateProvider : AuthenticationStateProvider { - // Do not change. This must match all versions of the server-side AuthenticationStateSerializer.PersistenceKey. - private const string PersistenceKey = $"__internal__{nameof(AuthenticationState)}"; + // restoring part is on DeserializedAuthenticationStateProvider but persisting part is on AuthenticationStateSerializer + // how can we make the key the same if these are two different classes in different assemblies? + // should we merge them and move to the Shared folder? Or should we allow passing a custom key to [SupplyParameterFromPersistentComponentState] attribute? + private readonly Func> _deserializeCallback; + + [SupplyParameterFromPersistentComponentState] + public AuthenticationStateData? CurrentAuthenticationState { get; set; } private static readonly Task _defaultUnauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); - private readonly Task _authenticationStateTask = _defaultUnauthenticatedTask; + public DeserializedAuthenticationStateProvider(IOptions options) + { + _deserializeCallback = options.Value.DeserializationCallback; + } - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = $"{nameof(DeserializedAuthenticationStateProvider)} uses the {nameof(DynamicDependencyAttribute)} to preserve the necessary members.")] - [DynamicDependency(JsonSerialized, typeof(AuthenticationStateData))] - [DynamicDependency(JsonSerialized, typeof(IList))] - [DynamicDependency(JsonSerialized, typeof(ClaimData))] - public DeserializedAuthenticationStateProvider(PersistentComponentState state, IOptions options) + public override Task GetAuthenticationStateAsync() { - if (!state.TryTakeFromJson(PersistenceKey, out var authenticationStateData) || authenticationStateData is null) + if (CurrentAuthenticationState is null) { - return; + return _defaultUnauthenticatedTask; } - - _authenticationStateTask = options.Value.DeserializationCallback(authenticationStateData); + var authenticationState = _deserializeCallback(CurrentAuthenticationState); + return authenticationState; } - - public override Task GetAuthenticationStateAsync() => _authenticationStateTask; } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs index 20cf1e0867f7..5f906f7ae7ac 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs @@ -5,6 +5,8 @@ using System.Reflection; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Infrastructure; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -29,7 +31,10 @@ public static class WebAssemblyAuthenticationServiceCollectionExtensions public static IServiceCollection AddAuthenticationStateDeserialization(this IServiceCollection services, Action? configure = null) { services.AddOptions(); - services.TryAddScoped(); + services.TryAddSingleton(); + services.TryAddSingleton(sp => (DeserializedAuthenticationStateProvider)sp.GetRequiredService()); + RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(services, RenderMode.InteractiveWebAssembly); + if (configure != null) { services.Configure(configure); From 4402b60a662ddfb8359488d64fdfe04c9e1e0a6c Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 19 May 2025 17:01:47 +0200 Subject: [PATCH 2/3] Cast registration to same public type. --- .../Server/src/AuthenticationStateSerializer.cs | 9 ++++++++- .../src/WebAssemblyRazorComponentsBuilderExtensions.cs | 6 +++--- ...bAssemblyAuthenticationServiceCollectionExtensions.cs | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs index b1f5488bc074..48df4755ced3 100644 --- a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs +++ b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs @@ -1,18 +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 System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Components.WebAssembly.Server; -internal sealed class AuthenticationStateSerializer : IHostEnvironmentAuthenticationStateProvider +internal sealed class AuthenticationStateSerializer : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider { private readonly Func> _serializeCallback; [SupplyParameterFromPersistentComponentState] public AuthenticationStateData? CurrentAuthenticationState { get; set; } + private static readonly Task _defaultUnauthenticatedTask = + Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + public AuthenticationStateSerializer(IOptions options) { _serializeCallback = options.Value.SerializationCallback; @@ -32,4 +36,7 @@ private async Task SetAuthenticationStateAsync(Task authent var authenticationState = await authenticationStateTask; CurrentAuthenticationState = await _serializeCallback(authenticationState); } + + public override Task GetAuthenticationStateAsync() + => _defaultUnauthenticatedTask; } diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs index 9d270ad25254..8c9852375d79 100644 --- a/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs +++ b/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs @@ -42,9 +42,9 @@ public static IRazorComponentsBuilder AddInteractiveWebAssemblyComponents(this I /// An that can be used to further customize the configuration. public static IRazorComponentsBuilder AddAuthenticationStateSerialization(this IRazorComponentsBuilder builder, Action? configure = null) { - builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped()); - builder.Services.TryAddScoped(sp => (AuthenticationStateSerializer)sp.GetRequiredService()); - RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(builder.Services, RenderMode.InteractiveAuto); + builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped()); + builder.Services.TryAddScoped(sp => (AuthenticationStateSerializer)sp.GetRequiredService()); + RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(builder.Services, RenderMode.InteractiveAuto); if (configure is not null) { builder.Services.Configure(configure); diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs index 5f906f7ae7ac..221173abd257 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs @@ -33,7 +33,7 @@ public static IServiceCollection AddAuthenticationStateDeserialization(this ISer services.AddOptions(); services.TryAddSingleton(); services.TryAddSingleton(sp => (DeserializedAuthenticationStateProvider)sp.GetRequiredService()); - RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(services, RenderMode.InteractiveWebAssembly); + RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(services, RenderMode.InteractiveWebAssembly); if (configure != null) { From 034e3ec384c1b59f76e83c766007e61fe44e7315 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 19 May 2025 17:04:21 +0200 Subject: [PATCH 3/3] Remove comments. --- .../WebAssembly/Server/src/AuthenticationStateSerializer.cs | 2 -- .../src/Services/DeserializedAuthenticationStateProvider.cs | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs index 48df4755ced3..482808b37a0f 100644 --- a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs +++ b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs @@ -26,8 +26,6 @@ public AuthenticationStateSerializer(IOptions authenticationStateTask) { ArgumentNullException.ThrowIfNull(authenticationStateTask, nameof(authenticationStateTask)); - - // fire and forget, not good... This method can throw, especially on serialization. _ = SetAuthenticationStateAsync(authenticationStateTask); } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs index a51240e62f72..ec954f1bee7c 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs @@ -9,9 +9,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication; internal sealed class DeserializedAuthenticationStateProvider : AuthenticationStateProvider { - // restoring part is on DeserializedAuthenticationStateProvider but persisting part is on AuthenticationStateSerializer - // how can we make the key the same if these are two different classes in different assemblies? - // should we merge them and move to the Shared folder? Or should we allow passing a custom key to [SupplyParameterFromPersistentComponentState] attribute? private readonly Func> _deserializeCallback; [SupplyParameterFromPersistentComponentState]