Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Endpoints" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Web" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Blazor.Build.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Authorization.Tests" />
Expand Down
4 changes: 4 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,7 @@ 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.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
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 attribute. If not specified, the property name will be used.
Comment thread
dariatiurina marked this conversation as resolved.
Outdated
/// </summary>
public string? Name { get; set; }

/// <inheritdoc />
internal override bool SingleDelivery => false;
}
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
6 changes: 6 additions & 0 deletions src/Components/Endpoints/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
#nullable enable
Microsoft.AspNetCore.Components.Endpoints.BasePath
Microsoft.AspNetCore.Components.Endpoints.BasePath.BasePath() -> void
Microsoft.AspNetCore.Components.Endpoints.ISessionValueMapper
Microsoft.AspNetCore.Components.Endpoints.ISessionValueMapper.DeleteValueCallback(string! sessionKey) -> void
Microsoft.AspNetCore.Components.Endpoints.ISessionValueMapper.GetValue(string! sessionKey, System.Type! targetType) -> object?
Microsoft.AspNetCore.Components.Endpoints.ISessionValueMapper.RegisterValueCallback(string! sessionKey, System.Func<object?>! valueGetter) -> void
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.TempDataCookie.get -> Microsoft.AspNetCore.Http.CookieBuilder!
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.TempDataCookie.set -> void
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.TempDataProviderType.get -> Microsoft.AspNetCore.Components.Endpoints.TempDataProviderType
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.TempDataProviderType.set -> void
Microsoft.AspNetCore.Components.Endpoints.SupplyParameterFromSessionServiceCollectionExtensions
Microsoft.AspNetCore.Components.Endpoints.TempDataProviderType
Microsoft.AspNetCore.Components.Endpoints.TempDataProviderType.Cookie = 0 -> Microsoft.AspNetCore.Components.Endpoints.TempDataProviderType
Microsoft.AspNetCore.Components.Endpoints.TempDataProviderType.SessionStorage = 1 -> Microsoft.AspNetCore.Components.Endpoints.TempDataProviderType
Expand All @@ -13,3 +18,4 @@ Microsoft.AspNetCore.Components.ITempData.Get(string! key) -> object?
Microsoft.AspNetCore.Components.ITempData.Keep() -> void
Microsoft.AspNetCore.Components.ITempData.Keep(string! key) -> void
Microsoft.AspNetCore.Components.ITempData.Peek(string! key) -> object?
static Microsoft.AspNetCore.Components.Endpoints.SupplyParameterFromSessionServiceCollectionExtensions.AddSupplyValueFromSessionProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public EndpointComponentState(Renderer renderer, int componentId, IComponent com

public bool StreamRendering { get; }

protected override object? GetComponentKey()
protected internal override object? GetComponentKey()
{
if (ParentComponentState != null && ParentComponentState.Component is SSRRenderModeBoundary boundary)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal partial class EndpointHtmlRenderer
{
private static readonly object ComponentSequenceKey = new object();

protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
protected internal override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
{
if (_isHandlingErrors)
{
Expand All @@ -42,7 +42,7 @@ protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessed
}
}

protected override IComponentRenderMode? GetComponentRenderMode(IComponent component)
protected internal override IComponentRenderMode? GetComponentRenderMode(IComponent component)
{
var componentState = GetComponentState(component);
var ssrRenderBoundary = GetClosestRenderModeBoundary(componentState);
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 Expand Up @@ -161,7 +166,7 @@ protected override ComponentState CreateComponentState(int componentId, ICompone
=> new EndpointComponentState(this, componentId, component, parentComponentState);

/// <inheritdoc/>
protected override ResourceAssetCollection Assets =>
protected internal override ResourceAssetCollection Assets =>
_resourceCollection ??= GetResourceCollection(_httpContext) ?? base.Assets;

private static ResourceAssetCollection? GetResourceCollection(HttpContext httpContext) => httpContext.GetEndpoint()?.Metadata.GetMetadata<ResourceAssetCollection>();
Expand Down
26 changes: 26 additions & 0 deletions src/Components/Endpoints/src/Session/ISessionValueMapper.cs
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.Endpoints;

/// <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);
}
115 changes: 115 additions & 0 deletions src/Components/Endpoints/src/Session/SessionValueMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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, List<Func<object?>>> _valueCallbacks = new();
Comment thread
dariatiurina marked this conversation as resolved.
Outdated
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 type)
{
var session = _httpContext?.Features.Get<ISessionFeature>()?.Session;
if (session is null)
{
return null;
}
try
{
var json = session.GetString(sessionKey);
Comment thread
dariatiurina marked this conversation as resolved.
Outdated
if (string.IsNullOrEmpty(json))
{
return null;
}
return JsonSerializer.Deserialize(json, type, _jsonOptions);
}
catch (JsonException ex)
{
Log.SessionDeserializeFail(_logger, ex);
return null;
}
}

public void RegisterValueCallback(string sessionKey, Func<object?> valueGetter)
{
if (!_valueCallbacks.TryGetValue(sessionKey, out var callbacks))
{
callbacks = new List<Func<object?>>();
_valueCallbacks[sessionKey] = callbacks;
}
callbacks.Add(valueGetter);
}

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, callbacks) in _valueCallbacks)
{
object? value = null;
foreach (var valueGetter in callbacks)
{
try
{
var candidateValue = valueGetter();
if (candidateValue is not null)
{
value = candidateValue;
break;
}
}
catch (Exception ex)
{
Log.SessionPersistFail(_logger, ex);
}
}
if (value is not null)
{
var json = JsonSerializer.Serialize(value, value.GetType(), _jsonOptions);
session.SetString(key, json);
Comment thread
dariatiurina marked this conversation as resolved.
Outdated
}
else
{
session.Remove(key);
Comment thread
dariatiurina marked this conversation as resolved.
Outdated
}
}
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);
}
}
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.Endpoints;

/// <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,64 @@
// 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.Endpoints;

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)
{
if (_sessionValueMapper is null)
{
return null;
}

Comment thread
dariatiurina marked this conversation as resolved.
Outdated
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);
}
}
Loading
Loading