-
Notifications
You must be signed in to change notification settings - Fork 10.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Root-level cascading values + CascadingAuthenticationState at root (#…
- Loading branch information
1 parent
2983a98
commit fdfb0d2
Showing
13 changed files
with
716 additions
and
2 deletions.
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
src/Components/Authorization/src/CascadingAuthenticationStateServiceCollectionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 _); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.