| File: SessionCascadingValueSupplier.cs | Web Access |
| Project: src\aspnetcore\src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj (Microsoft.AspNetCore.Components.Endpoints) |
// 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.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Components.Reflection; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Endpoints; internal partial class SessionCascadingValueSupplier { private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new(); private HttpContext? _httpContext; private readonly Dictionary<string, Func<object?>> _valueCallbacks = new(StringComparer.OrdinalIgnoreCase); private readonly IStoredDataSerializer _serializer; private readonly ILogger<SessionCascadingValueSupplier> _logger; public SessionCascadingValueSupplier(IStoredDataSerializer serializer, ILogger<SessionCascadingValueSupplier> logger) { _serializer = serializer; _logger = logger; } internal void SetRequestContext(HttpContext httpContext) { _httpContext = httpContext; } internal CascadingParameterSubscription CreateSubscription( ComponentState componentState, SupplyParameterFromSessionAttribute attribute, CascadingParameterInfo parameterInfo) { if (_httpContext is not null) { // Ensure that session cookie is issued to allow for persistence from streaming context SessionEstablishmentHelper.TryRegisterSessionEstablishment(_httpContext); } var sessionKey = (attribute.Name ?? parameterInfo.PropertyName).ToLowerInvariant(); var componentType = componentState.Component.GetType(); var propertyType = parameterInfo.PropertyType; if (propertyType != typeof(object) && !propertyType.IsAbstract && !_serializer.CanSerialize(propertyType)) { throw new InvalidOperationException( $"The property '{parameterInfo.PropertyName}' on component '{componentType}' is annotated with '[SupplyParameterFromSession]' but its type '{propertyType}' is not supported for session storage."); } var getter = _propertyGetterCache.GetOrAdd((componentType, parameterInfo.PropertyName), PropertyGetterFactory); Func<object?> valueGetter = () => getter.GetValue(componentState.Component); RegisterValueCallback(sessionKey, valueGetter); return new SessionSubscription(this, sessionKey, parameterInfo.PropertyType, valueGetter); } private static PropertyGetter PropertyGetterFactory((Type type, string propertyName) key) { var (type, propertyName) = key; var propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (propertyInfo is null) { throw new InvalidOperationException($"A property '{propertyName}' on component type '{type.FullName}' wasn't found."); } return new PropertyGetter(type, propertyInfo); } // A null HttpContext means we're rendering interactively (Server circuit or WebAssembly), // where the session isn't available; yield null instead of failing. When an HttpContext is // present (static SSR) an unavailable session is a misconfiguration and fails fast. internal ISession? GetSession() => _httpContext is null ? null : SessionResolver.GetRequiredSession(_httpContext); internal void RegisterValueCallback(string sessionKey, Func<object?> valueGetter) { sessionKey = sessionKey.ToLowerInvariant(); if (!_valueCallbacks.TryAdd(sessionKey, valueGetter)) { throw new InvalidOperationException($"A callback is already registered for the session key '{sessionKey}'. Multiple components cannot use the same session key for multiple [SupplyParameterFromSession] attributes."); } } internal Task PersistAllValues() { if (_valueCallbacks.Count == 0) { return Task.CompletedTask; } var session = GetSession(); if (session is null) { Log.SessionUnavailable(_logger); return Task.CompletedTask; } foreach (var (key, valueGetter) in _valueCallbacks) { object? value; try { value = valueGetter(); } catch (Exception ex) { Log.SessionPersistFail(_logger, ex); continue; } if (value is null) { session.Remove(key); continue; } var valueType = value.GetType(); var bytes = _serializer.SerializeValue(value, valueType); session.Set(key, bytes); } return Task.CompletedTask; } internal void DeleteValueCallback(string sessionKey) { _valueCallbacks.Remove(sessionKey); } private static partial class Log { [LoggerMessage(1, LogLevel.Warning, "Persisting of the session element failed.", EventName = "SessionPersistFail")] public static partial void SessionPersistFail(ILogger logger, Exception exception); [LoggerMessage(2, LogLevel.Warning, "Deserialization of the element from session failed.", EventName = "SessionDeserializeFail")] public static partial void SessionDeserializeFail(ILogger logger, Exception exception); [LoggerMessage(3, LogLevel.Warning, "No active HttpContext is available (interactive rendering); [SupplyParameterFromSession] is skipped.", EventName = "SessionUnavailable")] public static partial void SessionUnavailable(ILogger logger); } internal partial class SessionSubscription : CascadingParameterSubscription { private readonly SessionCascadingValueSupplier _owner; private readonly string _sessionKey; [DynamicallyAccessedMembers(LinkerFlags.JsonSerialized)] private readonly Type _propertyType; private readonly Func<object?> _currentValueGetter; private bool _delivered; public SessionSubscription( SessionCascadingValueSupplier owner, string sessionKey, [DynamicallyAccessedMembers(LinkerFlags.JsonSerialized)] Type propertyType, Func<object?> currentValueGetter) { _owner = owner; _sessionKey = sessionKey.ToLowerInvariant(); _propertyType = propertyType; _currentValueGetter = currentValueGetter; } public override object? GetCurrentValue() { if (_delivered) { return _currentValueGetter(); } _delivered = true; var session = _owner.GetSession(); if (session is null) { Log.SessionUnavailable(_owner._logger); return null; } try { if (!session.TryGetValue(_sessionKey, out var bytes) || bytes.Length == 0) { return null; } // The property type is known, so the serializer deserializes straight to it and recovers // the exact enum/collection type without consulting the stored token. return _owner._serializer.DeserializeValue(bytes, _propertyType); } catch (Exception ex) { Log.SessionDeserializeFail(_owner._logger, ex); return null; } } public override void Dispose() { _owner.DeleteValueCallback(_sessionKey); } } }