Skip to content

Commit

Permalink
Root-level cascading values + CascadingAuthenticationState at root (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSandersonMS authored Jul 5, 2023
1 parent 2983a98 commit fdfb0d2
Show file tree
Hide file tree
Showing 13 changed files with 716 additions and 2 deletions.
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 @@ -109,6 +116,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 @@ -119,6 +127,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

0 comments on commit fdfb0d2

Please sign in to comment.