Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
26 changes: 26 additions & 0 deletions src/Components/Components/src/ISessionValueMapper.cs
Comment thread
dariatiurina marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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;

/// <summary>
/// Maps session data values to a model.
/// </summary>
public interface ISessionValueMapper
{
/// <summary>
/// Returns the session value with the specified name, deserialized to the specified type.
/// </summary>
object? GetValue(string sessionKey, Type targetType);

/// <summary>
/// Registers a callback to retrieve the current value of a session property.
/// The callback will be invoked when the response starts to persist the value.
/// </summary>
void RegisterValueCallback(string sessionKey, Func<object?> valueGetter);

/// <summary>
/// Unregisters a previously registered callback for the specified session key.
/// </summary>
void DeleteValueCallback(string sessionKey);
}
10 changes: 10 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ Microsoft.AspNetCore.Components.IComponentPropertyActivator.GetActivator(System.
*REMOVED*Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties) -> void
*REMOVED*Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
*REMOVED*Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void
Microsoft.AspNetCore.Components.ISessionValueMapper
Microsoft.AspNetCore.Components.ISessionValueMapper.DeleteValueCallback(string! sessionKey) -> void
Microsoft.AspNetCore.Components.ISessionValueMapper.GetValue(string! sessionKey, System.Type! targetType) -> object?
Microsoft.AspNetCore.Components.ISessionValueMapper.RegisterValueCallback(string! sessionKey, System.Func<object?>! valueGetter) -> void
Microsoft.AspNetCore.Components.SupplyParameterFromSessionAttribute
Microsoft.AspNetCore.Components.SupplyParameterFromSessionAttribute.Name.get -> string?
Microsoft.AspNetCore.Components.SupplyParameterFromSessionAttribute.Name.set -> void
Microsoft.AspNetCore.Components.SupplyParameterFromSessionAttribute.SupplyParameterFromSessionAttribute() -> void
Comment thread
dariatiurina marked this conversation as resolved.
Outdated
Microsoft.AspNetCore.Components.SupplyParameterFromSessionServiceCollectionExtensions
static Microsoft.AspNetCore.Components.SupplyParameterFromSessionServiceCollectionExtensions.AddSupplyValueFromSessionProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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;

/// <summary>
/// Indicates that the value of the associated property should be supplied from
/// the session with the specified name.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class SupplyParameterFromSessionAttribute : CascadingParameterAttributeBase
{
/// <summary>
/// Gets or sets the name of the session key. If not specified, the property name will be used.
/// </summary>
public string? Name { get; set; }

/// <inheritdoc />
internal override bool SingleDelivery => false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Enables component parameters to be supplied from the session with <see cref="SupplyParameterFromSessionAttribute"/>.
/// </summary>
public static class SupplyParameterFromSessionServiceCollectionExtensions
{
/// <summary>
/// Enables component parameters to be supplied from the session with <see cref="SupplyParameterFromSessionAttribute"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddSupplyValueFromSessionProvider(this IServiceCollection services)
{
services.TryAddEnumerable(ServiceDescriptor.Scoped<ICascadingValueSupplier, SupplyParameterFromSessionValueProvider>());
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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.Reflection;
using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components;

internal class SupplyParameterFromSessionValueProvider : ICascadingValueSupplier
{
private readonly ISessionValueMapper _sessionValueMapper;

public SupplyParameterFromSessionValueProvider(ISessionValueMapper sessionValueMapper)
{
_sessionValueMapper = sessionValueMapper;
}

public bool IsFixed => false;

public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
=> parameterInfo.Attribute is SupplyParameterFromSessionAttribute;

public object? GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo)
{
var attribute = (SupplyParameterFromSessionAttribute)parameterInfo.Attribute;
var sessionKey = (attribute.Name ?? parameterInfo.PropertyName) ?? "";
return _sessionValueMapper.GetValue(sessionKey, parameterInfo.PropertyType);
}

[UnconditionalSuppressMessage(
"Trimming",
"IL2075:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute'",
Justification = "Component properties are preserved through other means.")]
public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
var attribute = (SupplyParameterFromSessionAttribute)parameterInfo.Attribute;
var sessionKey = (attribute.Name ?? parameterInfo.PropertyName) ?? "";
var propertyName = parameterInfo.PropertyName;
var componentType = subscriber.Component.GetType();
var propertyInfo = componentType.GetProperty(propertyName);

if (propertyInfo is null)
{
return;
}

var component = subscriber.Component;
var getter = new PropertyGetter(componentType, propertyInfo);
_sessionValueMapper.RegisterValueCallback(sessionKey, () => getter.GetValue(component));
}

public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
var attribute = (SupplyParameterFromSessionAttribute)parameterInfo.Attribute;
var sessionKey = (attribute.Name ?? parameterInfo.PropertyName) ?? "";
_sessionValueMapper.DeleteValueCallback(sessionKey);
Comment thread
dariatiurina marked this conversation as resolved.
Outdated
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
services.TryAddScoped<EndpointRoutingStateProvider>();
services.TryAddScoped<IRoutingStateProvider>(sp => sp.GetRequiredService<EndpointRoutingStateProvider>());
services.TryAddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.TryAddScoped<ISessionValueMapper, SessionValueMapper>();
services.AddSupplyValueFromQueryProvider();
services.AddSupplyValueFromSessionProvider();
services.AddSupplyValueFromPersistentComponentStateProvider();
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
services.TryAddScoped<WebAssemblySettingsEmitter>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ internal async Task InitializeStandardComponentServicesAsync(
antiforgery.SetRequestContext(httpContext);
}

if (httpContext.RequestServices.GetService<ISessionValueMapper>() is SessionValueMapper sessionValueMapper)
{
sessionValueMapper.SetRequestContext(httpContext);
}

// It's important that this is initialized since a component might try to restore state during prerendering
// (which will obviously not work, but should not fail)
var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
Expand Down
105 changes: 105 additions & 0 deletions src/Components/Endpoints/src/Session/SessionValueMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal partial class SessionValueMapper : ISessionValueMapper
{
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
private HttpContext? _httpContext;
private readonly Dictionary<string, Func<object?>> _valueCallbacks = new(StringComparer.OrdinalIgnoreCase);
private readonly ILogger<SessionValueMapper> _logger;

public SessionValueMapper(ILogger<SessionValueMapper> logger)
{
_logger = logger;
}

internal void SetRequestContext(HttpContext httpContext)
{
_httpContext = httpContext;
_httpContext.Response.OnStarting(PersistAllValues);
}

public object? GetValue(string sessionKey, Type targetType)
{
var session = _httpContext?.Features.Get<ISessionFeature>()?.Session;
if (session is null)
{
return null;
}
try
{
var json = session.GetString(sessionKey.ToLowerInvariant());
if (string.IsNullOrEmpty(json))
{
return null;
}
return JsonSerializer.Deserialize(json, targetType, _jsonOptions);
}
catch (JsonException ex)
{
Log.SessionDeserializeFail(_logger, ex);
return null;
}
}

public void RegisterValueCallback(string sessionKey, Func<object?> valueGetter)
{
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.");
}
}

public void DeleteValueCallback(string sessionKey)
{
_valueCallbacks.Remove(sessionKey);
}

private Task PersistAllValues()
{
var session = _httpContext?.Features.Get<ISessionFeature>()?.Session;
if (session is null)
{
return Task.CompletedTask;
}

foreach (var (key, valueGetter) in _valueCallbacks)
{
object? value = null;
try
{
value = valueGetter();
}
catch (Exception ex)
{
Log.SessionPersistFail(_logger, ex);
}
if (value is not null)
{
var json = JsonSerializer.Serialize(value, value.GetType(), _jsonOptions);
session.SetString(key.ToLowerInvariant(), json);
}
else
{
session.Remove(key.ToLowerInvariant());
}
}
return Task.CompletedTask;
}

private static partial class Log
{
[LoggerMessage(1, LogLevel.Warning, "Persisting of the 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, JsonException exception);
}
}
Loading
Loading