Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Root-level cascading values + CascadingAuthenticationState at root #49204

Merged
merged 16 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -0,0 +1,57 @@
// 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;
using Microsoft.AspNetCore.Components.Authorization;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods for configuring cascading authentication state on a service collection.
/// </summary>
public static class CascadingAuthenticationStateServiceCollectionExtensions
{
/// <summary>
/// Adds cascading authentication state to the <paramref name="serviceCollection"/>. This is equivalent to
/// having a <see cref="CascadingAuthenticationState"/> component at the root of your component hierarchy.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddCascadingAuthenticationState(this IServiceCollection serviceCollection)
{
return serviceCollection.AddCascadingValue<Task<AuthenticationState>>(services =>
{
var authenticationStateProvider = services.GetRequiredService<AuthenticationStateProvider>();
return new AuthenticationStateCascadingValueSource(authenticationStateProvider);
});
}

private sealed class AuthenticationStateCascadingValueSource : CascadingValueSource<Task<AuthenticationState>>, IDisposable
{
// This is intended to produce identical behavior to having a <CascadingAuthenticationStateProvider>
// wrapped around the root component.

private readonly AuthenticationStateProvider _authenticationStateProvider;

public AuthenticationStateCascadingValueSource(AuthenticationStateProvider authenticationStateProvider)
: base(authenticationStateProvider.GetAuthenticationStateAsync, isFixed: false)
{
_authenticationStateProvider = authenticationStateProvider;
_authenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
}

private void HandleAuthenticationStateChanged(Task<AuthenticationState> newAuthStateTask)
{
// It's OK to discard the task because this only represents the duration of the dispatch to sync context.
// It handles any exceptions internally by dispatching them to the renderer within the context of whichever
// component threw when receiving the update. This is the same as how a CascadingValue doesn't get notified
// about exceptions that happen inside the recipients of value notifications.
_ = NotifyChangedAsync(newAuthStateTask);
}

public void Dispose()
{
_authenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
}
}
}
2 changes: 2 additions & 0 deletions src/Components/Authorization/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.Extensions.DependencyInjection.CascadingAuthenticationStateServiceCollectionExtensions
static Microsoft.Extensions.DependencyInjection.CascadingAuthenticationStateServiceCollectionExtensions.AddCascadingAuthenticationState(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
10 changes: 10 additions & 0 deletions src/Components/Components/src/CascadingParameterState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com

private static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in CascadingParameterInfo info, ComponentState componentState)
{
// First scan up through the component hierarchy
var candidate = componentState;
do
{
Expand All @@ -68,6 +69,15 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
candidate = candidate.LogicalParentComponentState;
} while (candidate != null);

// We got to the root and found no match, so now look at the providers registered in DI
foreach (var valueSupplier in componentState.Renderer.ServiceProviderCascadingValueSuppliers)
{
if (valueSupplier.CanSupplyValue(info))
{
return valueSupplier;
}
}

// No match
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods for configuring cascading values on an <see cref="IServiceCollection"/>.
/// </summary>
public static class CascadingValueServiceCollectionExtensions
{
/// <summary>
/// Adds a cascading value to the <paramref name="serviceCollection"/>. This is equivalent to having
/// a fixed <see cref="CascadingValue{TValue}"/> at the root of the component hierarchy.
/// </summary>
/// <typeparam name="TValue">The value type.</typeparam>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
/// <param name="valueFactory">A callback that supplies a fixed value within each service provider scope.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddCascadingValue<TValue>(
this IServiceCollection serviceCollection, Func<IServiceProvider, TValue> valueFactory)
=> serviceCollection.AddScoped<ICascadingValueSupplier>(sp => new CascadingValueSource<TValue>(() => valueFactory(sp), isFixed: true));

/// <summary>
/// Adds a cascading value to the <paramref name="serviceCollection"/>. This is equivalent to having
/// a fixed <see cref="CascadingValue{TValue}"/> at the root of the component hierarchy.
/// </summary>
/// <typeparam name="TValue">The value type.</typeparam>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
/// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param>
/// <param name="valueFactory">A callback that supplies a fixed value within each service provider scope.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddCascadingValue<TValue>(
this IServiceCollection serviceCollection, string name, Func<IServiceProvider, TValue> valueFactory)
=> serviceCollection.AddScoped<ICascadingValueSupplier>(sp => new CascadingValueSource<TValue>(name, () => valueFactory(sp), isFixed: true));

/// <summary>
/// Adds a cascading value to the <paramref name="serviceCollection"/>. This is equivalent to having
/// a <see cref="CascadingValue{TValue}"/> at the root of the component hierarchy.
///
/// With this overload, you can supply a <see cref="CascadingValueSource{TValue}"/> which allows you
/// to notify about updates to the value later, causing recipients to re-render. This overload should
/// only be used if you plan to update the value dynamically.
/// </summary>
/// <typeparam name="TValue">The value type.</typeparam>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
/// <param name="sourceFactory">A callback that supplies a <see cref="CascadingValueSource{TValue}"/> within each service provider scope.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddCascadingValue<TValue>(
this IServiceCollection serviceCollection, Func<IServiceProvider, CascadingValueSource<TValue>> sourceFactory)
=> serviceCollection.AddScoped<ICascadingValueSupplier>(sourceFactory);
}
177 changes: 177 additions & 0 deletions src/Components/Components/src/CascadingValueSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// 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 Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Supplies a cascading value that can be received by components using
/// <see cref="CascadingParameterAttribute"/>.
/// </summary>
public class CascadingValueSource<TValue> : ICascadingValueSupplier
{
// By *not* making this sealed, people who want to deal with value disposal can subclass this,
// add IDisposable, and then do what they want during shutdown

private readonly ConcurrentDictionary<Dispatcher, List<ComponentState>>? _subscribers;
private readonly bool _isFixed;
private readonly string? _name;

// You can either provide an initial value to the constructor, or a func to provide one lazily
private TValue? _currentValue;
private Func<TValue>? _initialValueFactory;

/// <summary>
/// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>.
/// </summary>
/// <param name="value">The initial value.</param>
/// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param>
public CascadingValueSource(TValue value, bool isFixed) : this(isFixed)
{
_currentValue = value;
}

/// <summary>
/// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>.
/// </summary>
/// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param>
/// <param name="value">The initial value.</param>
/// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param>
public CascadingValueSource(string name, TValue value, bool isFixed) : this(value, isFixed)
{
ArgumentNullException.ThrowIfNull(name);
_name = name;
}

/// <summary>
/// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>.
/// </summary>
/// <param name="valueFactory">A callback that produces the initial value when first required.</param>
/// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param>
public CascadingValueSource(Func<TValue> valueFactory, bool isFixed) : this(isFixed)
{
_initialValueFactory = valueFactory;
}

/// <summary>
/// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>.
/// </summary>
/// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param>
/// <param name="valueFactory">A callback that produces the initial value when first required.</param>
/// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param>
public CascadingValueSource(string name, Func<TValue> valueFactory, bool isFixed) : this(valueFactory, isFixed)
{
ArgumentNullException.ThrowIfNull(name);
_name = name;
}

private CascadingValueSource(bool isFixed)
{
_isFixed = isFixed;

if (!_isFixed)
{
_subscribers = new();
}
}

/// <summary>
/// Notifies subscribers that the value has changed (for example, if it has been mutated).
/// </summary>
/// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns>
public Task NotifyChangedAsync()
{
if (_isFixed)
{
throw new InvalidOperationException($"Cannot notify about changes because the {GetType()} is configured as fixed.");
}

if (_subscribers?.Count > 0)
{
var tasks = new List<Task>();

foreach (var (dispatcher, subscribers) in _subscribers)
{
tasks.Add(dispatcher.InvokeAsync(() =>
{
foreach (var subscriber in subscribers)
{
subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
}
}));
}

return Task.WhenAll(tasks);
}
else
{
return Task.CompletedTask;
}
}

/// <summary>
/// Notifies subscribers that the value has changed, supplying a new value.
/// </summary>
/// <param name="newValue"></param>
/// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns>
public Task NotifyChangedAsync(TValue newValue)
{
_currentValue = newValue;
_initialValueFactory = null; // This definitely won't be used now

return NotifyChangedAsync();
}

bool ICascadingValueSupplier.IsFixed => _isFixed;

bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo)
{
if (parameterInfo.Attribute is not CascadingParameterAttribute cascadingParameterAttribute || !parameterInfo.PropertyType.IsAssignableFrom(typeof(TValue)))
{
return false;
}

// We only consider explicitly requested names, not the property name.
var requestedName = cascadingParameterAttribute.Name;
return (requestedName == null && _name == null) // Match on type alone
|| string.Equals(requestedName, _name, StringComparison.OrdinalIgnoreCase); // Also match on name
}

object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
{
if (_initialValueFactory is not null)
{
_currentValue = _initialValueFactory();
_initialValueFactory = null;
}

return _currentValue;
}

void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
Dispatcher dispatcher = subscriber.Renderer.Dispatcher;
dispatcher.AssertAccess();

// The .Add is threadsafe because we are in the sync context for this dispatcher
_subscribers?.GetOrAdd(dispatcher, _ => new()).Add(subscriber);
}

void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
{
Dispatcher dispatcher = subscriber.Renderer.Dispatcher;
dispatcher.AssertAccess();

if (_subscribers?.TryGetValue(dispatcher, out var subscribersForDispatcher) == true)
{
// Threadsafe because we're in the sync context for this dispatcher
subscribersForDispatcher.Remove(subscriber);
if (subscribersForDispatcher.Count == 0)
{
_subscribers.Remove(dispatcher, out _);
}
}
}
}
11 changes: 11 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ Microsoft.AspNetCore.Components.CascadingParameterInfo.Attribute.get -> Microsof
Microsoft.AspNetCore.Components.CascadingParameterInfo.CascadingParameterInfo() -> void
Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyName.get -> string!
Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyType.get -> System.Type!
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.CascadingValueSource(string! name, System.Func<TValue>! valueFactory, bool isFixed) -> void
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.CascadingValueSource(string! name, TValue value, bool isFixed) -> void
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.CascadingValueSource(System.Func<TValue>! valueFactory, bool isFixed) -> void
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.CascadingValueSource(TValue value, bool isFixed) -> void
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.NotifyChangedAsync() -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.NotifyChangedAsync(TValue newValue) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.IComponentRenderMode
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
Expand Down Expand Up @@ -103,6 +110,7 @@ Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool
Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void
*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string?
*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void
Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions
override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string?
override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void
override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int
Expand All @@ -113,6 +121,9 @@ override Microsoft.AspNetCore.Components.EventCallback<TValue>.Equals(object? ob
*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void
override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string?
override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.CascadingValueSource<TValue>!>! sourceFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Subscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void
virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Unsubscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask
Expand Down
Loading